From e5e199672903cfc4b927c41414ad843084dd6fcf Mon Sep 17 00:00:00 2001 From: Esurio/1673beta <60435625+1673beta@users.noreply.github.com> Date: Wed, 14 Aug 2024 18:25:08 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"0.5.0=E3=82=84=E3=82=8A=E7=9B=B4?= =?UTF-8?q?=E3=81=97=20(#146)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 413994126182e1b96561165fccb61286787d06da. --- .config/docker_example.env | 7 - .config/docker_example.yml | 35 - .config/example.yml | 103 +- .devcontainer/devcontainer.json | 17 +- .devcontainer/devcontainer.yml | 15 - .../{compose.yml => docker-compose.yml} | 4 +- .devcontainer/init.sh | 5 +- .dockerignore | 5 +- .github/FUNDING.yml | 3 + .github/ISSUE_TEMPLATE/01_bug-report.yml | 10 +- .github/ISSUE_TEMPLATE/02_feature-request.yml | 2 +- .github/ISSUE_TEMPLATE/03_dev.yml | 24 - .github/ISSUE_TEMPLATE/config.yml | 7 - .github/dependabot.yml | 30 +- .github/workflows/api-cherrypick-js.yml | 13 +- .github/workflows/changelog-check.yml | 43 - .../workflows/check-cherrypick-js-autogen.yml | 133 - .../workflows/check-cherrypick-js-version.yml | 29 - .github/workflows/check-spdx-license-id.yml | 74 - .github/workflows/deploy-test-environment.yml | 84 - .github/workflows/docker-develop.yml | 77 +- .github/workflows/docker.yml | 94 +- .github/workflows/dockle.yml | 8 +- .github/workflows/get-api-diff.yml | 10 +- .github/workflows/lint.yml | 64 +- .github/workflows/locale.yml | 28 - .github/workflows/ok-to-test.yml | 2 +- .github/workflows/on-release-created.yml | 42 - .github/workflows/pr-preview-deploy.yml | 6 +- .github/workflows/pr-preview-destroy.yml | 2 +- .github/workflows/release-edit-with-push.yml | 48 - .github/workflows/release-with-dispatch.yml | 127 - .github/workflows/release-with-ready.yml | 44 - .github/workflows/report-api-diff.yml | 2 +- .github/workflows/scorecard.yml | 73 - .github/workflows/storybook.yml | 111 - .github/workflows/test-backend.yml | 77 +- .github/workflows/test-cherrypick-js.yml | 13 +- .github/workflows/test-frontend.yml | 36 +- .github/workflows/test-production.yml | 9 +- .github/workflows/validate-api-json.yml | 45 - .gitignore | 7 +- .gitmodules | 3 + .node-version | 2 +- .vscode/extensions.json | 2 + .vscode/settings.json | 2 +- CHANGELOG.md | 380 +- CHANGELOG_CHERRYPICK.md | 89 +- CHANGELOG_engawa.md | 255 - CHANGELOG_yojo.md | 81 - CONTRIBUTING.md | 218 +- COPYING | 2 +- Dockerfile | 16 +- README.md | 43 +- ROADMAP.md | 1 - chart/files/default.yml | 16 - compose.local-db.yml | 75 - compose_example.yml | 134 - cypress/e2e/{basic.cy.ts => basic.cy.js} | 14 +- cypress/e2e/router.cy.ts | 35 - cypress/e2e/{widgets.cy.ts => widgets.cy.js} | 5 - cypress/support/{commands.ts => commands.js} | 6 +- cypress/support/{e2e.ts => e2e.js} | 0 cypress/support/index.ts | 19 - cypress/tsconfig.json | 8 - docker-compose.local-db.yml | 42 + docker-compose_example.yml | 67 + docs/Advanced-Search.md | 51 - docs/DEVELOPMENT.md | 45 - healthcheck.sh | 4 +- locales/ar-SA.yml | 31 +- locales/bn-BD.yml | 39 +- locales/ca-ES.yml | 1628 +- locales/cs-CZ.yml | 29 +- locales/da-DK.yml | 2 - locales/de-DE.yml | 81 +- locales/el-GR.yml | 6 +- locales/en-US.yml | 370 +- locales/es-ES.yml | 173 +- locales/fr-FR.yml | 151 +- locales/generateDTS.js | 213 +- locales/hr-HR.yml | 1 - locales/ht-HT.yml | 1 - locales/hu-HU.yml | 1 - locales/id-ID.yml | 393 +- locales/index.d.ts | 9108 +---- locales/index.js | 6 +- locales/it-IT.yml | 376 +- locales/ja-JP.yml | 375 +- locales/ja-KS.yml | 247 +- locales/jbo-EN.yml | 1 - locales/kab-KAB.yml | 4 - locales/kn-IN.yml | 1 - locales/ko-GS.yml | 174 +- locales/ko-KR.yml | 371 +- locales/lo-LA.yml | 257 +- locales/nl-NL.yml | 6 - locales/no-NO.yml | 7 - locales/pl-PL.yml | 176 +- locales/pt-PT.yml | 31 +- locales/ro-RO.yml | 12 - locales/ru-RU.yml | 121 +- locales/si-LK.yml | 18 - locales/sk-SK.yml | 17 +- locales/sv-SE.yml | 11 +- locales/th-TH.yml | 1758 +- locales/tr-TR.yml | 4 - locales/ug-CN.yml | 1 - locales/uk-UA.yml | 19 +- locales/uz-UZ.yml | 13 - locales/verify.js | 53 - locales/vi-VN.yml | 102 +- locales/zh-CN.yml | 608 +- locales/zh-TW.yml | 462 +- misskey-assets | 1 + package.json | 40 +- packages/backend/.eslintignore | 4 + packages/backend/.eslintrc.cjs | 32 + packages/backend/.swcrc | 3 +- packages/backend/assets/api-doc.html | 20 - packages/backend/assets/redoc.html | 24 + packages/backend/biome.json | 128 - .../backend/{scripts => }/check_connect.js | 4 +- packages/backend/eslint.config.js | 46 - packages/backend/generate_api_json.js | 8 + packages/backend/jest.config.cjs | 1 + packages/backend/jest.config.e2e.cjs | 15 - packages/backend/jest.config.unit.cjs | 14 - .../backend/migration/1000000000000-Init.js | 2 +- .../backend/migration/1556348509290-Pages.js | 2 +- .../migration/1556746559567-UserProfile.js | 2 +- .../migration/1557476068003-PinnedUsers.js | 2 +- .../migration/1557761316509-AddSomeUrls.js | 2 +- .../1557932705754-ObjectStorageSetting.js | 2 +- .../migration/1558072954435-PageLike.js | 2 +- .../migration/1558103093633-UserGroup.js | 2 +- .../1558257926829-UserGroupInvite.js | 2 +- .../1558266512381-UserListJoining.js | 2 +- .../migration/1561706992953-webauthn.js | 2 +- .../migration/1561873850023-ChartIndexes.js | 2 +- .../1562422242907-PasswordLessLogin.js | 2 +- .../migration/1562444565093-PinnedPage.js | 2 +- .../1562448332510-PageTitleHideOption.js | 2 +- .../migration/1562869971568-ModerationLog.js | 2 +- .../migration/1563757595828-UsedUsername.js | 2 +- .../backend/migration/1565634203341-room.js | 2 +- .../1571220798684-CustomEmojiCategory.js | 2 +- .../migration/1572760203493-nodeinfo.js | 2 +- .../1576269851876-TalkFederationId.js | 2 +- .../1576869585998-ProxyRemoteFiles.js | 2 +- .../backend/migration/1579267006611-v12.js | 2 +- .../backend/migration/1579270193251-v12-2.js | 2 +- .../backend/migration/1579282808087-v12-3.js | 2 +- .../backend/migration/1579544426412-v12-4.js | 2 +- .../backend/migration/1579977526288-v12-5.js | 2 +- .../backend/migration/1579993013959-v12-6.js | 2 +- .../backend/migration/1580069531114-v12-7.js | 2 +- .../backend/migration/1580148575182-v12-8.js | 2 +- .../backend/migration/1580154400017-v12-9.js | 2 +- .../backend/migration/1580276619901-v12-10.js | 2 +- .../backend/migration/1580331224276-v12-11.js | 2 +- .../backend/migration/1580508795118-v12-12.js | 2 +- .../backend/migration/1580543501339-v12-13.js | 2 +- .../backend/migration/1580864313253-v12-14.js | 2 +- .../1581526429287-user-group-invitation.js | 2 +- .../1581695816408-user-group-antenna.js | 2 +- ...581708415836-drive-user-folder-id-index.js | 2 +- .../backend/migration/1581979837262-promo.js | 2 +- .../1582019042083-featured-injecttion.js | 2 +- .../1582210532752-antenna-exclude.js | 2 +- .../1582875306439-note-reaction-length.js | 2 +- .../backend/migration/1585361548360-miauth.js | 2 +- .../1585385921215-custom-notification.js | 2 +- .../backend/migration/1585772678853-ap-url.js | 2 +- .../1586624197029-AddObjectStorageUseProxy.js | 2 +- .../1586641139527-remote-reaction.js | 2 +- .../migration/1586708940386-pageAiScript.js | 2 +- .../migration/1588044505511-hCaptcha.js | 2 +- .../migration/1589023282116-pubRelay.js | 2 +- .../migration/1595075960584-blurhash.js | 2 +- ...595077605646-blurhash-for-avatar-banner.js | 2 +- .../1595676934834-instance-icon-url.js | 2 +- .../migration/1595771249699-word-mute.js | 2 +- .../migration/1595782306083-word-mute2.js | 2 +- .../migration/1596548170836-channel.js | 2 +- .../migration/1596786425167-channel2.js | 2 +- ...597230137744-objectStorageSetPublicRead.js | 2 +- ...597236229720-IncludingNotificationTypes.js | 2 +- .../1597385880794-add-sensitive-index.js | 2 +- .../migration/1597459042300-channel-unread.js | 2 +- .../1597893996136-ChannelNoteIdDescIndex.js | 2 +- .../1600353287890-mutingNotificationTypes.js | 2 +- .../1603094348345-refine-abuse-user-report.js | 2 +- ...1603095701770-refine-abuse-user-report2.js | 2 +- .../1603776877564-instance-theme-color.js | 2 +- .../1603781553011-instance-favicon.js | 2 +- .../1604821689616-delete-auto-watch.js | 2 +- .../1605408848373-clip-description.js | 2 +- .../migration/1605408971051-comments.js | 2 +- .../1605585339718-instance-pinned-pages.js | 2 +- .../1605965516823-instance-images.js | 2 +- .../migration/1606191203881-no-crawle.js | 2 +- .../1607151207216-instance-pinned-clip.js | 2 +- .../migration/1607353487793-isExplorable.js | 2 +- .../migration/1610277136869-registry.js | 2 +- .../migration/1610277585759-registry2.js | 2 +- .../migration/1610283021566-registry3.js | 2 +- .../migration/1611354329133-followersUri.js | 2 +- .../migration/1611397665007-gallery.js | 2 +- ...547387175-objectStorageS3ForcePathStyle.js | 2 +- .../1612619156584-announcement-email.js | 2 +- .../1613155914446-emailNotificationTypes.js | 2 +- .../migration/1613181457597-user-lang.js | 2 +- ...1613503367223-use-bigint-for-driveUsage.js | 2 +- .../migration/1615965918224-chart-v2.js | 2 +- .../migration/1615966519402-chart-v2-2.js | 2 +- .../1618637372000-user-last-active-date.js | 2 +- .../1618639857000-user-hide-online-status.js | 2 +- .../migration/1619942102890-password-reset.js | 2 +- .../backend/migration/1620019354680-ad.js | 2 +- .../backend/migration/1620364649428-ad2.js | 2 +- .../1621479946000-add-note-indexes.js | 2 +- ...9304522-user-profile-description-length.js | 2 +- .../1622681548499-log-message-length.js | 2 +- .../1626509500668-fix-remote-file-proxy.js | 2 +- .../migration/1629004542760-chart-reindex.js | 2 +- .../1629024377804-deepl-integration.js | 2 +- .../1629288472000-fix-channel-userId.js | 2 +- .../1629387925000-disableRightClick.js | 2 +- .../1629512953000-user-is-deleted.js | 2 +- .../1629778475000-deepl-integration2.js | 2 +- .../1629833361000-AddShowTLReplies.js | 2 +- .../1629968054000_userInstanceBlocks.js | 2 +- ...1633068642000-email-required-for-signup.js | 2 +- .../migration/1633071909016-user-pending.js | 2 +- .../1634486652000-user-public-reactions.js | 2 +- .../migration/1634902659689-delete-log.js | 2 +- .../1635500777168-note-thread-mute.js | 2 +- .../migration/1636197624383-ff-visibility.js | 2 +- .../1636697408073-remove-via-mobile.js | 2 +- .../1637320813000-forwarded-report.js | 2 +- .../migration/1639325650583-chart-v3.js | 2 +- .../migration/1642611822809-emoji-url.js | 2 +- ...1642613870898-drive-file-webpublic-type.js | 2 +- .../migration/1643963705770-chart-v4.js | 2 +- .../migration/1643966656277-chart-v5.js | 2 +- .../migration/1643967331284-chart-v6.js | 2 +- .../1644010796173-convert-hard-mutes.js | 2 +- .../migration/1644058404077-chart-v7.js | 2 +- .../migration/1644059847460-chart-v8.js | 2 +- .../migration/1644060125705-chart-v9.js | 2 +- .../migration/1644073149413-chart-v10.js | 2 +- .../migration/1644095659741-chart-v11.js | 2 +- .../migration/1644328606241-chart-v12.js | 2 +- .../migration/1644331238153-chart-v13.js | 2 +- .../migration/1644344266289-chart-v14.js | 2 +- .../1644395759931-instance-theme-color.js | 2 +- .../migration/1644481657998-chart-v15.js | 2 +- .../1644551208096-following-indexes.js | 2 +- ...45340161439-remove-max-note-text-length.js | 2 +- .../1645599900873-federation-chart-pubsub.js | 2 +- .../1646143552768-instance-default-theme.js | 2 +- .../1646387162108-mute-expires-at.js | 2 +- .../1646549089451-poll-ended-notification.js | 2 +- .../1646633030285-chart-federation-active.js | 2 +- ...655454495-remove-instance-drive-columns.js | 2 +- ...2390560-chart-federation-active-sub-pub.js | 2 +- .../migration/1648548247382-webhook.js | 2 +- .../migration/1648816172177-webhook-2.js | 2 +- .../migration/1651224615271-foreign-key.js | 2 +- .../1652859567549-uniform-themecolor.js | 2 +- .../1654589815249-increase-smtp-char-limit.js | 2 +- .../migration/1655368940105-nsfw-detection.js | 2 +- .../1655371960534-nsfw-detection-2.js | 2 +- .../1655388169582-nsfw-detection-3.js | 2 +- .../1655393015659-nsfw-detection-4.js | 2 +- .../1655813815729-driveCapacityOverrideMb.js | 2 +- .../migration/1655918165614-user-ip.js | 2 +- .../migration/1656122560740-file-ip.js | 2 +- .../1656251734807-nsfw-detection-5.js | 2 +- .../backend/migration/1656328812281-ip-2.js | 2 +- .../1656408772602-nsfw-detection-6.js | 2 +- .../1656772790599-user-moderation-note.js | 2 +- .../1657346559800-active-email-validation.js | 2 +- ...60183643857-multipleTranslationServices.js | 2 +- .../migration/1664694635394-turnstile.js | 2 +- .../1665091090561-add-renote-muting.js | 2 +- ...6634-whetherPushNotifyToSendReadMessage.js | 2 +- .../1671924750884-RetentionAggregation.js | 2 +- .../1671926422832-RetentionAggregation2.js | 2 +- .../migration/1672562400597-PerUserPvChart.js | 2 +- ...672703171386-remove-latestRequestSentAt.js | 2 +- ...1672704017999-remove-lastCommunicatedAt.js | 2 +- .../1672704136584-remove-latestStatus.js | 2 +- .../backend/migration/1672822262496-Flash.js | 2 +- .../1673336077243-PollChoiceLength.js | 2 +- .../backend/migration/1673500412259-Role.js | 2 +- .../migration/1673515526953-RoleColor.js | 2 +- .../migration/1673522856499-RoleIroiro.js | 2 +- .../migration/1673524604156-RoleLastUsedAt.js | 2 +- .../1673570377815-RoleConditional.js | 2 +- .../migration/1673575973645-MetaClean.js | 2 +- .../migration/1673783015567-Policies.js | 2 +- .../1673812883772-firstRetrievedAt.js | 2 +- .../1674086433654-flashScriptLength.js | 2 +- .../migration/1674118260469-achievement.js | 2 +- .../migration/1674255666603-loggedInDates.js | 2 +- .../1675053125067-fixforeignkeyreports.js | 2 +- .../migration/1675404035646-cleanup.js | 2 +- .../1675557528704-role-icon-badge.js | 2 +- .../backend/migration/1676438468213-ad3.js | 2 +- .../backend/migration/1677054292210-ad4.js | 2 +- ...677570181236-role-assignment-expires-at.js | 2 +- ...8164627293-per-note-reaction-acceptance.js | 2 +- .../1678426061773-tweak-varchar-length.js | 2 +- .../migration/1678427401214-remove-unused.js | 2 +- .../1678602320354-role-display-order.js | 2 +- .../1678694614599-sensitive-words.js | 2 +- .../1678869617549-retention-date-key.js | 2 +- ...678945242650-add-props-for-custom-emoji.js | 2 +- .../migration/1678953978856-clip-favorite.js | 2 +- .../migration/1679309757174-antenna-active.js | 2 +- ...1679639483253-enableChartsForRemoteUser.js | 2 +- .../migration/1679651580149-cleanup.js | 2 +- ...81809-enableChartsForFederatedInstances.js | 2 +- .../1680228513388-channelFavorite.js | 2 +- .../1680238118084-channelNotePining.js | 2 +- .../migration/1680491187535-cleanup.js | 2 +- .../migration/1680582195041-cleanup.js | 2 +- .../migration/1680702787050-UserMemo.js | 2 +- ...1680775031481-avatar-url-and-banner-url.js | 2 +- .../migration/1680931179228-account-move.js | 2 +- .../migration/1681400427971-serverRules.js | 2 +- .../backend/migration/1681429921400-Event.js | 2 +- .../backend/migration/1681673280586-event.js | 2 +- .../backend/migration/1681675881633-event.js | 2 +- .../migration/1681870960239-RoleTLSetting.js | 2 +- .../migration/1682190963894-movedAt.js | 2 +- .../1682754135458-preservedUsernames.js | 2 +- .../migration/1682985520254-channelColor.js | 2 +- .../migration/1683328299359-channelArchive.js | 2 +- .../1683682889948-prevent-ai-larning.js | 2 +- ...683083083-public-reactions-default-true.js | 2 +- .../migration/1683789676867-fix-typo.js | 2 +- .../migration/1683847157541-UserList.js | 2 +- .../1683869758873-UserListFavorites.js | 2 +- ...684206886988-remove-showTimelineReplies.js | 2 +- .../1684231006000-tweak-varchar-length2.js | 2 +- .../migration/1684386446061-emoji-improve.js | 2 +- ...85378242713-multipleTranslationServices.js | 2 +- .../migration/1685973839966-errorImageUrl.js | 2 +- .../1686908762393-AbuseReportResolver.js | 2 +- .../1688280713783-add-meta-options.js | 2 +- .../1688720440658-refactor-invite-system.js | 2 +- .../1688880985544-add-index-to-relations.js | 2 +- .../migration/1689102832143-nsfw-cache.js | 2 +- .../1689325027964-UserBlacklistAnntena.js | 5 - ...689580926821-ObjectStorageRemoteSetting.js | 2 +- .../1690417561185-fix-renote-muting.js | 5 - ...417561186-ChangeCacheRemoteFilesDefault.js | 5 - .../backend/migration/1690417561187-Fix.js | 5 - .../1690569881926-user-2fa-backup-codes.js | 5 - .../1690782653311-SensitiveChannel.js | 2 +- .../1690796169261-play-visibility.js | 2 +- ...82-notification-emails-for-abuse-report.js | 2 +- .../1691649257651-refine-announcement.js | 5 - .../1691657412740-refine-announcement-2.js | 5 - .../1691959191872-passkey-support.js | 2 +- ...1694850832075-server-icons-and-manifest.js | 2 +- .../migration/1694915420864-clipped-count.js | 2 +- .../migration/1695260774117-verified-links.js | 5 - .../1695288787870-following-notify.js | 5 - .../migration/1695440131671-short-name.js | 5 - .../1695605508898-mutingNotificationTypes.js | 5 - .../1695901659683-note-updated-at.js | 5 - ...1695944637565-notificationRecieveConfig.js | 2 +- .../migration/1696003580220-AddSomeUrls.js | 2 +- .../1696044626209-noteEditHistory.js | 2 +- .../migration/1696222183852-withReplies.js | 2 +- .../1696318192428-noteUpdatedAtHistory.js | 2 +- .../1696323464251-user-list-membership.js | 5 - .../migration/1696331570827-hibernation.js | 5 - .../backend/migration/1696332072038-clean.js | 5 - .../1696373953614-meta-cache-settings.js | 2 +- .../1696402675000-add-meta-options.js | 2 +- .../migration/1696405744672-clean-up.js | 2 +- .../1696417300000-add-meta-options.js | 2 +- .../migration/1696569742153-clean-up.js | 2 +- .../migration/1696581429196-clean-up.js | 2 +- .../migration/1696604572677-poll-vote-poll.js | 5 - .../migration/1696743032098-AdsOnStream.js | 2 +- .../migration/1696807733453-userListUserId.js | 2 +- .../1696808725134-userListUserId-2.js | 2 +- .../1697247230117-InstanceSilence.js | 2 +- .../1697420555911-deleteCreatedAt.js | 2 +- .../1697436246389-antenna-localOnly.js | 2 +- .../1697441463087-FollowRequestWithReplies.js | 2 +- ...673894459-note-reactionAndUserPairCache.js | 2 +- .../1697737204579-deleteCreatedAt.js | 4 - .../1697847397844-avatar-decoration.js | 2 +- .../1697941908548-avatar-decoration2.js | 2 +- .../migration/1698041201306-enable-ftt.js | 2 +- ...8840138000-add-allow-renote-to-external.js | 2 +- .../1699141698112-announcement-silence.js | 2 +- .../1699432324194-remoteAvaterDecoration.js | 5 - ...96812223-enableFanoutTimelineDbFallback.js | 2 +- .../1700303245007-supportVerifyMailApi.js | 2 +- .../migration/1700383825690-hard-mute.js | 5 - .../migration/1700902349231-add-bday-index.js | 2 +- .../migration/1702718871541-ffVisibility.js | 2 +- .../1703209889304-bannedEmailDomains.js | 2 +- .../1703658526000-supportTrueMailApi.js | 20 - .../1704185628000-note-updated-at2.js | 5 - .../1704373210054-support-mcaptcha.js | 22 - .../1704959805077-bubble-game-record.js | 24 - ...58-optimize-note-index-for-array-column.js | 24 - .../migration/1705475608437-reversi.js | 22 - .../migration/1705654039457-reversi-2.js | 18 - .../migration/1705793785675-reversi-3.js | 18 - .../migration/1705794768153-reversi-4.js | 18 - .../migration/1705798904141-reversi-5.js | 16 - .../migration/1706081514499-reversi-6.js | 16 - ...6791962000-fix-meta-disableRegistration.js | 16 - .../1707429690000-prohibited-words.js | 16 - ...1707808106310-MakeRepositoryUrlNullable.js | 16 - ...epositoryUrl-from-syuilo-to-misskey-dev.js | 16 - .../1708399372194-per-instance-mod-note.js | 16 - .../migration/1710210658917-AddSomeUrl.js | 15 - .../1710512074000-url-preview-meta.js | 42 - .../1710919614510-antenna-exclude-bots.js | 16 - .../1711722198590-no-recursive-delete.js | 14 - ...1713656541000-abuse-report-notification.js | 62 - ...29964060-ChannelIdDenormalizedForMiPoll.js | 21 - .../1716197366117-MediaSilenceForHosts.js | 16 - .../1716345015347-NotRespondingSince.js | 16 - ...8870-SuspensionStateInsteadOfIsSspended.js | 50 - .../1716450883149-RemoveAntennaNotify.js | 16 - .../migration/1717117195275-inquiryUrl.js | 16 - .../1717644139656-addDirectSummalyProxy.js | 15 - .../migration/1720161864577-AddDeleteAt.js | 11 - .../migration/1721299883211-AddIsIndexable.js | 13 - .../1721550461923-AddPrivateVisibility.js | 11 - .../migration/1721666053703-fixDriveUrl.js | 24 - .../migration/1722350613009-AddIsSensitive.js | 11 - .../1722353293595-AddSensitiveProfile.js | 11 - packages/backend/package.json | 206 +- packages/backend/scripts/dev.mjs | 63 - packages/backend/scripts/generate_api_json.js | 36 - packages/backend/src/@types/hcaptcha.d.ts | 2 +- .../backend/src/@types/http-signature.d.ts | 2 +- packages/backend/src/@types/os-utils.d.ts | 2 +- packages/backend/src/@types/package.json.d.ts | 2 +- .../backend/src/@types/probe-image-size.d.ts | 2 +- packages/backend/src/@types/redis-lock.d.ts | 2 +- packages/backend/src/GlobalModule.ts | 47 +- packages/backend/src/MainModule.ts | 2 +- packages/backend/src/NestLogger.ts | 4 +- packages/backend/src/boot/common.ts | 2 +- packages/backend/src/boot/entry.ts | 7 +- packages/backend/src/boot/master.ts | 24 +- packages/backend/src/boot/ready.ts | 6 - packages/backend/src/boot/worker.ts | 25 +- packages/backend/src/config.ts | 73 +- packages/backend/src/const.ts | 5 +- .../core/AbuseReportNotificationService.ts | 405 - .../backend/src/core/AbuseReportService.ts | 128 - .../backend/src/core/AccountMoveService.ts | 12 +- .../backend/src/core/AccountUpdateService.ts | 2 +- .../backend/src/core/AchievementService.ts | 5 +- packages/backend/src/core/AiService.ts | 2 +- .../backend/src/core/AnnouncementService.ts | 55 +- packages/backend/src/core/AntennaService.ts | 26 +- packages/backend/src/core/AppLockService.ts | 2 +- .../src/core/AvatarDecorationService.ts | 104 +- packages/backend/src/core/CacheService.ts | 79 +- packages/backend/src/core/CaptchaService.ts | 33 +- .../src/core/ChannelFollowingService.ts | 5 - packages/backend/src/core/ClipService.ts | 10 +- packages/backend/src/core/CoreModule.ts | 97 +- .../src/core/CreateSystemUserService.ts | 7 +- .../backend/src/core/CustomEmojiService.ts | 22 +- .../backend/src/core/DeleteAccountService.ts | 6 +- packages/backend/src/core/DownloadService.ts | 5 +- packages/backend/src/core/DriveService.ts | 94 +- packages/backend/src/core/EmailService.ts | 131 +- .../src/core/FanoutTimelineEndpointService.ts | 40 +- .../backend/src/core/FanoutTimelineService.ts | 2 +- packages/backend/src/core/FeaturedService.ts | 2 +- .../src/core/FederatedInstanceService.ts | 7 +- .../src/core/FetchInstanceMetadataService.ts | 32 +- packages/backend/src/core/FileInfoService.ts | 60 +- .../backend/src/core/GlobalEventService.ts | 49 +- packages/backend/src/core/HashtagService.ts | 4 +- .../backend/src/core/HttpRequestService.ts | 57 +- packages/backend/src/core/IdService.ts | 2 +- .../src/core/ImageProcessingService.ts | 2 +- .../backend/src/core/InstanceActorService.ts | 12 +- .../src/core/InternalStorageService.ts | 2 +- packages/backend/src/core/LoggerService.ts | 6 +- packages/backend/src/core/MessagingService.ts | 6 +- packages/backend/src/core/MetaService.ts | 7 +- packages/backend/src/core/MfmService.ts | 63 +- .../backend/src/core/ModerationLogService.ts | 2 +- .../backend/src/core/NoteCreateService.ts | 135 +- .../backend/src/core/NoteDeleteService.ts | 19 +- .../backend/src/core/NotePiningService.ts | 2 +- packages/backend/src/core/NoteReadService.ts | 70 +- .../backend/src/core/NoteUpdateService.ts | 17 +- .../backend/src/core/NotificationService.ts | 55 +- packages/backend/src/core/PollService.ts | 2 +- .../backend/src/core/ProxyAccountService.ts | 2 +- .../src/core/PushNotificationService.ts | 11 +- packages/backend/src/core/QueryService.ts | 8 +- packages/backend/src/core/QueueModule.ts | 65 +- packages/backend/src/core/QueueService.ts | 87 +- packages/backend/src/core/ReactionService.ts | 79 +- .../backend/src/core/RegistryApiService.ts | 2 +- packages/backend/src/core/RelayService.ts | 6 +- .../backend/src/core/RemoteLoggerService.ts | 2 +- .../src/core/RemoteUserResolveService.ts | 2 +- packages/backend/src/core/RoleService.ts | 114 +- packages/backend/src/core/S3Service.ts | 2 +- packages/backend/src/core/SearchService.ts | 200 +- packages/backend/src/core/SignupService.ts | 20 +- .../backend/src/core/SystemWebhookService.ts | 233 - packages/backend/src/core/UserAuthService.ts | 2 +- .../backend/src/core/UserBlockingService.ts | 12 +- .../backend/src/core/UserFollowingService.ts | 97 +- .../backend/src/core/UserKeypairService.ts | 2 +- packages/backend/src/core/UserListService.ts | 4 +- .../backend/src/core/UserMutingService.ts | 2 +- .../src/core/UserRenoteMutingService.ts | 52 - .../backend/src/core/UserSearchService.ts | 205 - packages/backend/src/core/UserService.ts | 26 +- .../backend/src/core/UserSuspendService.ts | 2 +- .../backend/src/core/UserWebhookService.ts | 99 - packages/backend/src/core/UtilityService.ts | 28 +- .../src/core/VideoProcessingService.ts | 2 +- packages/backend/src/core/WebAuthnService.ts | 28 +- packages/backend/src/core/WebfingerService.ts | 2 +- packages/backend/src/core/WebhookService.ts | 94 + .../src/core/activitypub/ApAudienceService.ts | 8 +- .../core/activitypub/ApDbResolverService.ts | 13 +- .../activitypub/ApDeliverManagerService.ts | 4 +- .../src/core/activitypub/ApInboxService.ts | 155 +- .../src/core/activitypub/ApLoggerService.ts | 2 +- .../src/core/activitypub/ApMfmService.ts | 21 +- .../src/core/activitypub/ApRendererService.ts | 79 +- .../src/core/activitypub/ApRequestService.ts | 21 +- .../src/core/activitypub/ApResolverService.ts | 4 +- ...JsonLdService.ts => LdSignatureService.ts} | 40 +- .../src/core/activitypub/misc/contexts.ts | 43 +- .../src/core/activitypub/misc/validator.ts | 39 - .../core/activitypub/models/ApEventService.ts | 2 +- .../core/activitypub/models/ApImageService.ts | 21 +- .../activitypub/models/ApMentionService.ts | 4 +- .../core/activitypub/models/ApNoteService.ts | 112 +- .../activitypub/models/ApPersonService.ts | 49 +- .../activitypub/models/ApQuestionService.ts | 8 +- .../src/core/activitypub/models/icon.ts | 2 +- .../src/core/activitypub/models/identifier.ts | 2 +- .../src/core/activitypub/models/tag.ts | 4 +- packages/backend/src/core/activitypub/type.ts | 15 +- .../src/core/chart/ChartLoggerService.ts | 4 +- .../src/core/chart/ChartManagementService.ts | 2 +- .../src/core/chart/charts/active-users.ts | 2 +- .../src/core/chart/charts/ap-request.ts | 2 +- .../backend/src/core/chart/charts/drive.ts | 2 +- .../chart/charts/entities/active-users.ts | 2 +- .../core/chart/charts/entities/ap-request.ts | 2 +- .../src/core/chart/charts/entities/drive.ts | 2 +- .../core/chart/charts/entities/federation.ts | 2 +- .../core/chart/charts/entities/instance.ts | 2 +- .../src/core/chart/charts/entities/notes.ts | 2 +- .../chart/charts/entities/per-user-drive.ts | 2 +- .../charts/entities/per-user-following.ts | 2 +- .../chart/charts/entities/per-user-notes.ts | 2 +- .../core/chart/charts/entities/per-user-pv.ts | 2 +- .../charts/entities/per-user-reactions.ts | 2 +- .../chart/charts/entities/test-grouped.ts | 2 +- .../charts/entities/test-intersection.ts | 2 +- .../core/chart/charts/entities/test-unique.ts | 2 +- .../src/core/chart/charts/entities/test.ts | 2 +- .../src/core/chart/charts/entities/users.ts | 2 +- .../src/core/chart/charts/federation.ts | 8 +- .../backend/src/core/chart/charts/instance.ts | 2 +- .../backend/src/core/chart/charts/notes.ts | 2 +- .../src/core/chart/charts/per-user-drive.ts | 2 +- .../core/chart/charts/per-user-following.ts | 2 +- .../src/core/chart/charts/per-user-notes.ts | 2 +- .../src/core/chart/charts/per-user-pv.ts | 2 +- .../core/chart/charts/per-user-reactions.ts | 2 +- .../src/core/chart/charts/test-grouped.ts | 2 +- .../core/chart/charts/test-intersection.ts | 2 +- .../src/core/chart/charts/test-unique.ts | 2 +- .../backend/src/core/chart/charts/test.ts | 2 +- .../backend/src/core/chart/charts/users.ts | 2 +- packages/backend/src/core/chart/core.ts | 67 +- packages/backend/src/core/chart/entities.ts | 2 +- .../backend/src/core/deserializeAntenna.ts | 15 - ...eportNotificationRecipientEntityService.ts | 87 - .../entities/AbuseUserReportEntityService.ts | 41 +- .../entities/AnnouncementEntityService.ts | 71 - .../src/core/entities/AntennaEntityService.ts | 5 +- .../src/core/entities/AppEntityService.ts | 2 +- .../core/entities/AuthSessionEntityService.ts | 2 +- .../core/entities/BlockingEntityService.ts | 18 +- .../src/core/entities/ChannelEntityService.ts | 6 +- .../src/core/entities/ClipEntityService.ts | 22 +- .../core/entities/DriveFileEntityService.ts | 31 +- .../core/entities/DriveFolderEntityService.ts | 2 +- .../src/core/entities/EmojiEntityService.ts | 3 +- .../src/core/entities/FlashEntityService.ts | 18 +- .../core/entities/FlashLikeEntityService.ts | 2 +- .../entities/FollowRequestEntityService.ts | 29 +- .../core/entities/FollowingEntityService.ts | 30 +- .../core/entities/GalleryLikeEntityService.ts | 2 +- .../core/entities/GalleryPostEntityService.ts | 16 +- .../src/core/entities/HashtagEntityService.ts | 2 +- .../core/entities/InstanceEntityService.ts | 15 +- .../core/entities/InviteCodeEntityService.ts | 26 +- .../entities/MessagingMessageEntityService.ts | 2 +- .../src/core/entities/MetaEntityService.ts | 174 - .../entities/ModerationLogEntityService.ts | 21 +- .../src/core/entities/MutingEntityService.ts | 18 +- .../src/core/entities/NoteEntityService.ts | 40 +- .../entities/NoteFavoriteEntityService.ts | 2 +- .../entities/NoteReactionEntityService.ts | 24 +- .../entities/NotificationEntityService.ts | 325 +- .../src/core/entities/PageEntityService.ts | 18 +- .../core/entities/PageLikeEntityService.ts | 2 +- .../entities/RenoteMutingEntityService.ts | 18 +- .../src/core/entities/RoleEntityService.ts | 2 +- .../src/core/entities/SigninEntityService.ts | 2 +- .../entities/SystemWebhookEntityService.ts | 74 - .../src/core/entities/UserEntityService.ts | 325 +- .../core/entities/UserGroupEntityService.ts | 2 +- .../UserGroupInvitationEntityService.ts | 2 +- .../core/entities/UserListEntityService.ts | 7 +- packages/backend/src/daemons/DaemonModule.ts | 2 +- .../backend/src/daemons/QueueStatsService.ts | 2 +- .../backend/src/daemons/ServerStatsService.ts | 4 +- packages/backend/src/decorators.ts | 2 +- packages/backend/src/di-symbols.ts | 6 +- packages/backend/src/env.ts | 2 +- packages/backend/src/global.d.ts | 2 +- packages/backend/src/logger.ts | 23 +- packages/backend/src/misc/FileWriterStream.ts | 36 - packages/backend/src/misc/JsonArrayStream.ts | 35 - packages/backend/src/misc/acct.ts | 2 +- packages/backend/src/misc/cache.ts | 46 +- packages/backend/src/misc/check-https.ts | 2 +- packages/backend/src/misc/check-word-mute.ts | 2 +- packages/backend/src/misc/clone.ts | 6 +- .../backend/src/misc/content-disposition.ts | 2 +- packages/backend/src/misc/correct-filename.ts | 4 +- packages/backend/src/misc/create-temp.ts | 2 +- packages/backend/src/misc/dev-null.ts | 2 +- packages/backend/src/misc/emoji-regex.ts | 2 +- .../misc/extract-custom-emojis-from-mfm.ts | 2 +- packages/backend/src/misc/extract-hashtags.ts | 2 +- packages/backend/src/misc/extract-mentions.ts | 2 +- .../backend/src/misc/fastify-hook-handlers.ts | 14 - .../backend/src/misc/fastify-reply-error.ts | 2 +- packages/backend/src/misc/flash-token.ts | 2 +- packages/backend/src/misc/gen-identicon.ts | 11 +- packages/backend/src/misc/gen-key-pair.ts | 2 +- .../backend/src/misc/generate-invite-code.ts | 2 +- .../src/misc/generate-native-user-token.ts | 2 +- packages/backend/src/misc/get-ip-hash.ts | 2 +- packages/backend/src/misc/get-note-summary.ts | 2 +- .../backend/src/misc/get-reaction-emoji.ts | 2 +- packages/backend/src/misc/i18n.ts | 2 +- packages/backend/src/misc/id/aid.ts | 2 +- packages/backend/src/misc/id/aidx.ts | 2 +- packages/backend/src/misc/id/meid.ts | 2 +- packages/backend/src/misc/id/meidg.ts | 2 +- packages/backend/src/misc/id/object-id.ts | 2 +- packages/backend/src/misc/id/ulid.ts | 2 +- .../backend/src/misc/identifiable-error.ts | 2 +- .../src/misc/is-duplicate-key-value-error.ts | 2 +- .../backend/src/misc/is-instance-muted.ts | 2 +- packages/backend/src/misc/is-mime-image.ts | 2 +- packages/backend/src/misc/is-native-token.ts | 2 +- packages/backend/src/misc/is-not-null.ts | 10 + packages/backend/src/misc/is-pure-renote.ts | 10 + packages/backend/src/misc/is-quote.ts | 12 + packages/backend/src/misc/is-renote.ts | 67 - packages/backend/src/misc/is-reply.ts | 2 +- packages/backend/src/misc/is-user-related.ts | 6 +- packages/backend/src/misc/json-schema.ts | 52 +- packages/backend/src/misc/json-value.ts | 8 - packages/backend/src/misc/langmap.ts | 2 +- packages/backend/src/misc/loader.ts | 5 - .../backend/src/misc/normalize-for-search.ts | 2 +- packages/backend/src/misc/password.ts | 20 - packages/backend/src/misc/prelude/array.ts | 40 +- .../backend/src/misc/prelude/await-all.ts | 2 +- packages/backend/src/misc/prelude/math.ts | 8 + packages/backend/src/misc/prelude/maybe.ts | 25 + packages/backend/src/misc/prelude/relation.ts | 2 +- packages/backend/src/misc/prelude/string.ts | 20 + packages/backend/src/misc/prelude/symbol.ts | 6 + packages/backend/src/misc/prelude/time.ts | 2 +- packages/backend/src/misc/prelude/url.ts | 2 +- packages/backend/src/misc/prelude/xml.ts | 2 +- packages/backend/src/misc/promise-tracker.ts | 23 - packages/backend/src/misc/reset-db.ts | 2 +- packages/backend/src/misc/safe-for-sql.ts | 2 +- packages/backend/src/misc/secure-rndstr.ts | 2 +- .../backend/src/misc/show-machine-info.ts | 2 +- packages/backend/src/misc/sql-like-escape.ts | 4 +- packages/backend/src/misc/status-error.ts | 4 +- packages/backend/src/misc/truncate.ts | 2 +- .../AbuseReportNotificationRecipient.ts | 100 - .../backend/src/models/AbuseReportResolver.ts | 2 +- .../backend/src/models/AbuseUserReport.ts | 2 +- packages/backend/src/models/AccessToken.ts | 2 +- packages/backend/src/models/Ad.ts | 2 +- packages/backend/src/models/Announcement.ts | 6 +- .../backend/src/models/AnnouncementRead.ts | 2 +- packages/backend/src/models/Antenna.ts | 10 +- packages/backend/src/models/App.ts | 2 +- packages/backend/src/models/AuthSession.ts | 2 +- .../backend/src/models/AvatarDecoration.ts | 2 +- packages/backend/src/models/Blocking.ts | 2 +- .../backend/src/models/BubbleGameRecord.ts | 57 - packages/backend/src/models/Channel.ts | 2 +- .../backend/src/models/ChannelFavorite.ts | 2 +- .../backend/src/models/ChannelFollowing.ts | 2 +- packages/backend/src/models/Clip.ts | 2 +- packages/backend/src/models/ClipFavorite.ts | 2 +- packages/backend/src/models/ClipNote.ts | 2 +- packages/backend/src/models/DriveFile.ts | 8 +- packages/backend/src/models/DriveFolder.ts | 2 +- packages/backend/src/models/Emoji.ts | 2 +- packages/backend/src/models/Event.ts | 2 +- packages/backend/src/models/Flash.ts | 2 +- packages/backend/src/models/FlashLike.ts | 2 +- packages/backend/src/models/FollowRequest.ts | 2 +- packages/backend/src/models/Following.ts | 2 +- packages/backend/src/models/GalleryLike.ts | 2 +- packages/backend/src/models/GalleryPost.ts | 2 +- packages/backend/src/models/Hashtag.ts | 2 +- packages/backend/src/models/Instance.ts | 24 +- .../backend/src/models/MessagingMessage.ts | 2 +- packages/backend/src/models/Meta.ts | 104 +- packages/backend/src/models/ModerationLog.ts | 2 +- packages/backend/src/models/Muting.ts | 2 +- packages/backend/src/models/Note.ts | 22 +- packages/backend/src/models/NoteFavorite.ts | 2 +- packages/backend/src/models/NoteReaction.ts | 2 +- .../backend/src/models/NoteThreadMuting.ts | 2 +- packages/backend/src/models/NoteUnread.ts | 2 +- packages/backend/src/models/Notification.ts | 12 +- packages/backend/src/models/Page.ts | 2 +- packages/backend/src/models/PageLike.ts | 2 +- .../src/models/PasswordResetRequest.ts | 2 +- packages/backend/src/models/Poll.ts | 11 +- packages/backend/src/models/PollVote.ts | 2 +- packages/backend/src/models/PromoNote.ts | 2 +- packages/backend/src/models/PromoRead.ts | 2 +- .../backend/src/models/RegistrationTicket.ts | 2 +- packages/backend/src/models/RegistryItem.ts | 2 +- packages/backend/src/models/Relay.ts | 2 +- packages/backend/src/models/RenoteMuting.ts | 2 +- .../backend/src/models/RepositoryModule.ts | 255 +- .../src/models/RetentionAggregation.ts | 2 +- packages/backend/src/models/Role.ts | 98 +- packages/backend/src/models/RoleAssignment.ts | 2 +- packages/backend/src/models/Signin.ts | 2 +- packages/backend/src/models/SwSubscription.ts | 2 +- packages/backend/src/models/SystemWebhook.ts | 100 - packages/backend/src/models/UsedUsername.ts | 2 +- packages/backend/src/models/User.ts | 16 +- packages/backend/src/models/UserGroup.ts | 2 +- .../backend/src/models/UserGroupInvitation.ts | 2 +- .../backend/src/models/UserGroupJoining.ts | 2 +- packages/backend/src/models/UserIp.ts | 2 +- packages/backend/src/models/UserKeypair.ts | 2 +- packages/backend/src/models/UserList.ts | 2 +- .../backend/src/models/UserListFavorite.ts | 2 +- .../backend/src/models/UserListMembership.ts | 2 +- packages/backend/src/models/UserMemo.ts | 2 +- packages/backend/src/models/UserNotePining.ts | 2 +- packages/backend/src/models/UserPending.ts | 2 +- packages/backend/src/models/UserProfile.ts | 16 +- packages/backend/src/models/UserPublickey.ts | 2 +- .../backend/src/models/UserSecurityKey.ts | 2 +- packages/backend/src/models/Webhook.ts | 2 +- packages/backend/src/models/_.ts | 205 +- .../abuse-report-notification-recipient.ts | 50 - packages/backend/src/models/json-schema/ad.ts | 2 +- .../src/models/json-schema/announcement.ts | 4 +- .../backend/src/models/json-schema/antenna.ts | 10 +- .../backend/src/models/json-schema/app.ts | 2 +- .../src/models/json-schema/blocking.ts | 4 +- .../backend/src/models/json-schema/channel.ts | 2 +- .../backend/src/models/json-schema/clip.ts | 6 +- .../src/models/json-schema/drive-file.ts | 4 +- .../src/models/json-schema/drive-folder.ts | 2 +- .../backend/src/models/json-schema/emoji.ts | 6 +- .../models/json-schema/federation-instance.ts | 15 +- .../backend/src/models/json-schema/flash.ts | 2 +- .../src/models/json-schema/following.ts | 6 +- .../src/models/json-schema/gallery-post.ts | 2 +- .../backend/src/models/json-schema/hashtag.ts | 2 +- .../src/models/json-schema/invite-code.ts | 2 +- .../models/json-schema/messaging-message.ts | 2 +- .../backend/src/models/json-schema/meta.ts | 350 - .../backend/src/models/json-schema/muting.ts | 4 +- .../src/models/json-schema/note-favorite.ts | 2 +- .../src/models/json-schema/note-reaction.ts | 2 +- .../backend/src/models/json-schema/note.ts | 75 +- .../src/models/json-schema/notification.ts | 417 +- .../backend/src/models/json-schema/page.ts | 105 +- .../backend/src/models/json-schema/queue.ts | 2 +- .../src/models/json-schema/renote-muting.ts | 4 +- .../backend/src/models/json-schema/role.ts | 335 +- .../backend/src/models/json-schema/signin.ts | 5 - .../src/models/json-schema/system-webhook.ts | 54 - .../src/models/json-schema/user-group.ts | 2 +- .../src/models/json-schema/user-list.ts | 2 +- .../backend/src/models/json-schema/user.ts | 188 +- packages/backend/src/models/util/id.ts | 2 +- packages/backend/src/postgres.ts | 46 +- .../backend/src/queue/QueueLoggerService.ts | 2 +- .../backend/src/queue/QueueProcessorModule.ts | 12 +- .../src/queue/QueueProcessorService.ts | 555 +- packages/backend/src/queue/const.ts | 6 +- .../AggregateRetentionProcessorService.ts | 2 +- .../CheckExpiredMutingsProcessorService.ts | 2 +- .../processors/CleanChartsProcessorService.ts | 2 +- .../queue/processors/CleanProcessorService.ts | 2 +- .../CleanRemoteFilesProcessorService.ts | 4 +- .../DeleteAccountProcessorService.ts | 6 +- .../DeleteDriveFilesProcessorService.ts | 2 +- .../processors/DeleteFileProcessorService.ts | 2 +- .../processors/DeliverProcessorService.ts | 26 +- .../EndedPollNotificationProcessorService.ts | 13 +- .../ExportAntennasProcessorService.ts | 4 +- .../ExportBlockingProcessorService.ts | 2 +- .../processors/ExportClipsProcessorService.ts | 204 - .../ExportCustomEmojisProcessorService.ts | 2 +- .../ExportFavoritesProcessorService.ts | 2 +- .../ExportFollowingProcessorService.ts | 2 +- .../ExportMutingProcessorService.ts | 2 +- .../processors/ExportNotesProcessorService.ts | 169 +- .../ExportUserListsProcessorService.ts | 2 +- .../ImportAntennasProcessorService.ts | 12 +- .../ImportBlockingProcessorService.ts | 2 +- .../ImportCustomEmojisProcessorService.ts | 2 +- .../ImportFollowingProcessorService.ts | 2 +- .../ImportMutingProcessorService.ts | 2 +- .../ImportUserListsProcessorService.ts | 6 +- .../queue/processors/InboxProcessorService.ts | 72 +- .../RelationshipProcessorService.ts | 2 +- .../processors/ReportAbuseProcessorService.ts | 2 +- .../ResyncChartsProcessorService.ts | 2 +- .../ScheduledNoteDeleteProcessorService.ts | 45 - .../SystemWebhookDeliverProcessorService.ts | 87 - .../processors/TickChartsProcessorService.ts | 2 +- ...e.ts => WebhookDeliverProcessorService.ts} | 10 +- packages/backend/src/queue/types.ts | 22 +- .../src/server/ActivityPubServerService.ts | 10 +- .../backend/src/server/FileServerService.ts | 154 +- .../backend/src/server/HealthServerService.ts | 54 - .../src/server/NodeinfoServerService.ts | 6 +- packages/backend/src/server/ServerModule.ts | 11 +- packages/backend/src/server/ServerService.ts | 29 +- .../src/server/WellKnownServerService.ts | 2 +- .../backend/src/server/api/ApiCallService.ts | 113 +- .../src/server/api/ApiLoggerService.ts | 2 +- .../src/server/api/ApiServerService.ts | 6 +- .../src/server/api/AuthenticateService.ts | 2 +- .../backend/src/server/api/EndpointsModule.ts | 85 +- .../backend/src/server/api/FtsQueryService.ts | 49 - .../backend/src/server/api/GetterService.ts | 2 +- .../src/server/api/RateLimiterService.ts | 38 +- .../src/server/api/SigninApiService.ts | 13 +- .../backend/src/server/api/SigninService.ts | 6 +- .../src/server/api/SignupApiService.ts | 24 +- .../server/api/StreamingApiServerService.ts | 2 +- .../backend/src/server/api/endpoint-base.ts | 2 +- packages/backend/src/server/api/endpoints.ts | 69 +- .../admin/abuse-report-resolver/create.ts | 30 +- .../admin/abuse-report-resolver/delete.ts | 5 +- .../admin/abuse-report-resolver/list.ts | 5 +- .../admin/abuse-report-resolver/update.ts | 5 +- .../notification-recipient/create.ts | 122 - .../notification-recipient/delete.ts | 44 - .../notification-recipient/list.ts | 55 - .../notification-recipient/show.ts | 64 - .../notification-recipient/update.ts | 128 - .../api/endpoints/admin/abuse-user-reports.ts | 8 +- .../api/endpoints/admin/accounts/create.ts | 19 +- .../api/endpoints/admin/accounts/delete.ts | 2 +- .../endpoints/admin/accounts/find-by-email.ts | 6 +- .../server/api/endpoints/admin/ad/create.ts | 6 +- .../server/api/endpoints/admin/ad/delete.ts | 2 +- .../src/server/api/endpoints/admin/ad/list.ts | 2 +- .../server/api/endpoints/admin/ad/update.ts | 8 +- .../endpoints/admin/announcements/create.ts | 2 +- .../endpoints/admin/announcements/delete.ts | 2 +- .../api/endpoints/admin/announcements/list.ts | 11 +- .../endpoints/admin/announcements/update.ts | 2 +- .../admin/avatar-decorations/create.ts | 2 +- .../admin/avatar-decorations/delete.ts | 2 +- .../admin/avatar-decorations/list.ts | 2 +- .../admin/avatar-decorations/update.ts | 2 +- .../api/endpoints/admin/delete-account.ts | 5 +- .../admin/delete-all-files-of-a-user.ts | 2 +- .../admin/drive/clean-remote-files.ts | 2 +- .../api/endpoints/admin/drive/cleanup.ts | 2 +- .../server/api/endpoints/admin/drive/files.ts | 2 +- .../api/endpoints/admin/drive/show-file.ts | 22 +- .../endpoints/admin/emoji/add-aliases-bulk.ts | 2 +- .../server/api/endpoints/admin/emoji/add.ts | 7 +- .../server/api/endpoints/admin/emoji/adds.ts | 7 +- .../server/api/endpoints/admin/emoji/copy.ts | 2 +- .../api/endpoints/admin/emoji/delete-bulk.ts | 2 +- .../api/endpoints/admin/emoji/delete.ts | 2 +- .../api/endpoints/admin/emoji/import-zip.ts | 2 +- .../api/endpoints/admin/emoji/list-remote.ts | 2 +- .../server/api/endpoints/admin/emoji/list.ts | 2 +- .../admin/emoji/remove-aliases-bulk.ts | 2 +- .../endpoints/admin/emoji/set-aliases-bulk.ts | 2 +- .../admin/emoji/set-category-bulk.ts | 2 +- .../endpoints/admin/emoji/set-license-bulk.ts | 2 +- .../server/api/endpoints/admin/emoji/steal.ts | 2 +- .../api/endpoints/admin/emoji/update.ts | 29 +- .../admin/federation/delete-all-files.ts | 2 +- .../refresh-remote-instance-metadata.ts | 2 +- .../admin/federation/remove-all-following.ts | 2 +- .../admin/federation/update-instance.ts | 26 +- .../api/endpoints/admin/get-index-stats.ts | 2 +- .../api/endpoints/admin/get-table-stats.ts | 14 +- .../api/endpoints/admin/get-user-ips.ts | 2 +- .../api/endpoints/admin/invite/create.ts | 6 +- .../server/api/endpoints/admin/invite/list.ts | 2 +- .../api/endpoints/admin/invite/revoke.ts | 2 +- .../src/server/api/endpoints/admin/meta.ts | 99 +- .../api/endpoints/admin/promo/create.ts | 4 +- .../server/api/endpoints/admin/queue/clear.ts | 2 +- .../endpoints/admin/queue/deliver-delayed.ts | 2 +- .../endpoints/admin/queue/inbox-delayed.ts | 2 +- .../api/endpoints/admin/queue/promote.ts | 2 +- .../server/api/endpoints/admin/queue/stats.ts | 7 +- .../server/api/endpoints/admin/relays/add.ts | 2 +- .../server/api/endpoints/admin/relays/list.ts | 2 +- .../api/endpoints/admin/relays/remove.ts | 2 +- .../api/endpoints/admin/reset-password.ts | 12 +- .../admin/resolve-abuse-user-report.ts | 55 +- .../api/endpoints/admin/roles/assign.ts | 2 +- .../api/endpoints/admin/roles/create.ts | 2 +- .../api/endpoints/admin/roles/delete.ts | 2 +- .../server/api/endpoints/admin/roles/list.ts | 2 +- .../server/api/endpoints/admin/roles/show.ts | 2 +- .../api/endpoints/admin/roles/unassign.ts | 2 +- .../admin/roles/update-default-policies.ts | 2 +- .../api/endpoints/admin/roles/update.ts | 16 +- .../server/api/endpoints/admin/roles/users.ts | 9 +- .../server/api/endpoints/admin/send-email.ts | 2 +- .../server/api/endpoints/admin/server-info.ts | 2 +- .../api/endpoints/admin/set-user-sensitive.ts | 71 - .../endpoints/admin/show-moderation-logs.ts | 4 +- .../server/api/endpoints/admin/show-user.ts | 162 +- .../server/api/endpoints/admin/show-users.ts | 6 +- .../api/endpoints/admin/suspend-user.ts | 2 +- .../endpoints/admin/system-webhook/create.ts | 85 - .../endpoints/admin/system-webhook/delete.ts | 44 - .../endpoints/admin/system-webhook/list.ts | 60 - .../endpoints/admin/system-webhook/show.ts | 62 - .../endpoints/admin/system-webhook/update.ts | 91 - .../api/endpoints/admin/unset-user-avatar.ts | 2 +- .../api/endpoints/admin/unset-user-banner.ts | 2 +- .../endpoints/admin/unset-user-sensitive.ts | 71 - .../api/endpoints/admin/unsuspend-user.ts | 2 +- .../server/api/endpoints/admin/update-meta.ts | 130 +- .../api/endpoints/admin/update-user-note.ts | 2 +- .../src/server/api/endpoints/announcements.ts | 13 +- .../api/endpoints/announcements/show.ts | 54 - .../server/api/endpoints/antennas/create.ts | 14 +- .../server/api/endpoints/antennas/delete.ts | 2 +- .../src/server/api/endpoints/antennas/list.ts | 2 +- .../server/api/endpoints/antennas/notes.ts | 12 +- .../src/server/api/endpoints/antennas/show.ts | 2 +- .../server/api/endpoints/antennas/update.ts | 18 +- .../src/server/api/endpoints/ap/get.ts | 2 +- .../src/server/api/endpoints/ap/show.ts | 4 +- .../src/server/api/endpoints/app/create.ts | 6 +- .../src/server/api/endpoints/app/show.ts | 2 +- .../src/server/api/endpoints/auth/accept.ts | 4 +- .../api/endpoints/auth/session/generate.ts | 6 +- .../server/api/endpoints/auth/session/show.ts | 2 +- .../api/endpoints/auth/session/userkey.ts | 4 +- .../server/api/endpoints/blocking/create.ts | 27 +- .../server/api/endpoints/blocking/delete.ts | 6 +- .../src/server/api/endpoints/blocking/list.ts | 2 +- .../api/endpoints/bubble-game/ranking.ts | 83 - .../api/endpoints/bubble-game/register.ts | 89 - .../server/api/endpoints/channels/create.ts | 6 +- .../server/api/endpoints/channels/favorite.ts | 2 +- .../server/api/endpoints/channels/featured.ts | 2 +- .../server/api/endpoints/channels/follow.ts | 2 +- .../server/api/endpoints/channels/followed.ts | 2 +- .../api/endpoints/channels/my-favorites.ts | 2 +- .../server/api/endpoints/channels/owned.ts | 2 +- .../server/api/endpoints/channels/search.ts | 2 +- .../src/server/api/endpoints/channels/show.ts | 2 +- .../server/api/endpoints/channels/timeline.ts | 3 +- .../api/endpoints/channels/unfavorite.ts | 2 +- .../server/api/endpoints/channels/unfollow.ts | 2 +- .../server/api/endpoints/channels/update.ts | 2 +- .../api/endpoints/charts/active-users.ts | 2 +- .../server/api/endpoints/charts/ap-request.ts | 2 +- .../src/server/api/endpoints/charts/drive.ts | 2 +- .../server/api/endpoints/charts/federation.ts | 2 +- .../server/api/endpoints/charts/instance.ts | 2 +- .../src/server/api/endpoints/charts/notes.ts | 2 +- .../server/api/endpoints/charts/user/drive.ts | 2 +- .../api/endpoints/charts/user/following.ts | 2 +- .../server/api/endpoints/charts/user/notes.ts | 2 +- .../server/api/endpoints/charts/user/pv.ts | 2 +- .../api/endpoints/charts/user/reactions.ts | 2 +- .../src/server/api/endpoints/charts/users.ts | 2 +- .../server/api/endpoints/clips/add-note.ts | 2 +- .../src/server/api/endpoints/clips/create.ts | 2 +- .../src/server/api/endpoints/clips/delete.ts | 2 +- .../server/api/endpoints/clips/favorite.ts | 4 +- .../src/server/api/endpoints/clips/list.ts | 2 +- .../api/endpoints/clips/my-favorites.ts | 2 +- .../src/server/api/endpoints/clips/notes.ts | 2 +- .../server/api/endpoints/clips/remove-note.ts | 2 +- .../src/server/api/endpoints/clips/show.ts | 2 +- .../server/api/endpoints/clips/unfavorite.ts | 2 +- .../src/server/api/endpoints/clips/update.ts | 6 +- .../backend/src/server/api/endpoints/drive.ts | 2 +- .../src/server/api/endpoints/drive/files.ts | 4 +- .../endpoints/drive/files/attached-notes.ts | 4 +- .../endpoints/drive/files/check-existence.ts | 4 +- .../api/endpoints/drive/files/create.ts | 8 +- .../api/endpoints/drive/files/delete.ts | 2 +- .../api/endpoints/drive/files/find-by-hash.ts | 2 +- .../server/api/endpoints/drive/files/find.ts | 4 +- .../server/api/endpoints/drive/files/show.ts | 2 +- .../api/endpoints/drive/files/update.ts | 2 +- .../endpoints/drive/files/upload-from-url.ts | 2 +- .../src/server/api/endpoints/drive/folders.ts | 2 +- .../api/endpoints/drive/folders/create.ts | 6 +- .../api/endpoints/drive/folders/delete.ts | 2 +- .../api/endpoints/drive/folders/find.ts | 2 +- .../api/endpoints/drive/folders/show.ts | 2 +- .../api/endpoints/drive/folders/update.ts | 11 +- .../src/server/api/endpoints/drive/stream.ts | 2 +- .../api/endpoints/email-address/available.ts | 2 +- .../backend/src/server/api/endpoints/emoji.ts | 2 +- .../src/server/api/endpoints/emojis.ts | 2 +- .../src/server/api/endpoints/endpoint.ts | 2 +- .../src/server/api/endpoints/endpoints.ts | 2 +- .../api/endpoints/export-custom-emojis.ts | 2 +- .../api/endpoints/federation/followers.ts | 2 +- .../api/endpoints/federation/following.ts | 2 +- .../api/endpoints/federation/instances.ts | 13 +- .../api/endpoints/federation/show-instance.ts | 4 +- .../server/api/endpoints/federation/stats.ts | 2 +- .../federation/update-remote-user.ts | 2 +- .../server/api/endpoints/federation/users.ts | 4 +- .../api/endpoints/fetch-external-resources.ts | 2 +- .../src/server/api/endpoints/fetch-rss.ts | 177 +- .../src/server/api/endpoints/flash/create.ts | 8 +- .../src/server/api/endpoints/flash/delete.ts | 2 +- .../server/api/endpoints/flash/featured.ts | 2 +- .../server/api/endpoints/flash/gen-token.ts | 2 +- .../src/server/api/endpoints/flash/like.ts | 4 +- .../server/api/endpoints/flash/my-likes.ts | 2 +- .../src/server/api/endpoints/flash/my.ts | 2 +- .../src/server/api/endpoints/flash/show.ts | 2 +- .../src/server/api/endpoints/flash/unlike.ts | 2 +- .../src/server/api/endpoints/flash/update.ts | 14 +- .../server/api/endpoints/following/create.ts | 15 +- .../server/api/endpoints/following/delete.ts | 4 +- .../api/endpoints/following/invalidate.ts | 2 +- .../endpoints/following/requests/accept.ts | 2 +- .../endpoints/following/requests/cancel.ts | 2 +- .../api/endpoints/following/requests/list.ts | 4 +- .../endpoints/following/requests/reject.ts | 2 +- .../api/endpoints/following/update-all.ts | 2 +- .../server/api/endpoints/following/update.ts | 2 +- .../server/api/endpoints/gallery/featured.ts | 2 +- .../server/api/endpoints/gallery/popular.ts | 2 +- .../src/server/api/endpoints/gallery/posts.ts | 2 +- .../api/endpoints/gallery/posts/create.ts | 8 +- .../api/endpoints/gallery/posts/delete.ts | 2 +- .../api/endpoints/gallery/posts/like.ts | 4 +- .../api/endpoints/gallery/posts/show.ts | 2 +- .../api/endpoints/gallery/posts/unlike.ts | 2 +- .../api/endpoints/gallery/posts/update.ts | 28 +- .../api/endpoints/get-avatar-decorations.ts | 2 +- .../api/endpoints/get-online-users-count.ts | 2 +- .../src/server/api/endpoints/hashtags/list.ts | 2 +- .../server/api/endpoints/hashtags/search.ts | 4 +- .../src/server/api/endpoints/hashtags/show.ts | 2 +- .../server/api/endpoints/hashtags/trend.ts | 2 +- .../server/api/endpoints/hashtags/users.ts | 8 +- .../backend/src/server/api/endpoints/i.ts | 6 +- .../src/server/api/endpoints/i/2fa/done.ts | 17 +- .../server/api/endpoints/i/2fa/key-done.ts | 14 +- .../api/endpoints/i/2fa/password-less.ts | 4 +- .../api/endpoints/i/2fa/register-key.ts | 9 +- .../server/api/endpoints/i/2fa/register.ts | 6 +- .../server/api/endpoints/i/2fa/remove-key.ts | 8 +- .../server/api/endpoints/i/2fa/unregister.ts | 8 +- .../server/api/endpoints/i/2fa/update-key.ts | 4 +- .../src/server/api/endpoints/i/apps.ts | 7 +- .../server/api/endpoints/i/authorized-apps.ts | 8 +- .../server/api/endpoints/i/change-password.ts | 9 +- .../api/endpoints/i/claim-achievement.ts | 2 +- .../server/api/endpoints/i/delete-account.ts | 6 +- .../server/api/endpoints/i/export-antennas.ts | 2 +- .../server/api/endpoints/i/export-blocking.ts | 2 +- .../server/api/endpoints/i/export-clips.ts | 35 - .../api/endpoints/i/export-favorites.ts | 2 +- .../api/endpoints/i/export-following.ts | 2 +- .../src/server/api/endpoints/i/export-mute.ts | 2 +- .../server/api/endpoints/i/export-notes.ts | 2 +- .../api/endpoints/i/export-user-lists.ts | 2 +- .../src/server/api/endpoints/i/favorites.ts | 2 +- .../server/api/endpoints/i/gallery/likes.ts | 2 +- .../server/api/endpoints/i/gallery/posts.ts | 2 +- .../server/api/endpoints/i/import-antennas.ts | 6 +- .../server/api/endpoints/i/import-blocking.ts | 4 +- .../api/endpoints/i/import-following.ts | 4 +- .../server/api/endpoints/i/import-muting.ts | 4 +- .../api/endpoints/i/import-user-lists.ts | 4 +- .../src/server/api/endpoints/i/move.ts | 9 +- .../api/endpoints/i/notifications-grouped.ts | 17 +- .../server/api/endpoints/i/notifications.ts | 72 +- .../src/server/api/endpoints/i/page-likes.ts | 2 +- .../src/server/api/endpoints/i/pages.ts | 2 +- .../backend/src/server/api/endpoints/i/pin.ts | 6 +- .../i/read-all-messaging-messages.ts | 2 +- .../api/endpoints/i/read-all-unread-notes.ts | 2 +- .../api/endpoints/i/read-announcement.ts | 2 +- .../api/endpoints/i/regenerate-token.ts | 6 +- .../api/endpoints/i/registry/get-all.ts | 2 +- .../api/endpoints/i/registry/get-detail.ts | 13 +- .../server/api/endpoints/i/registry/get.ts | 2 +- .../endpoints/i/registry/keys-with-type.ts | 5 +- .../server/api/endpoints/i/registry/keys.ts | 9 +- .../server/api/endpoints/i/registry/remove.ts | 2 +- .../i/registry/scopes-with-domain.ts | 2 +- .../server/api/endpoints/i/registry/set.ts | 2 +- .../server/api/endpoints/i/revoke-token.ts | 6 +- .../server/api/endpoints/i/signin-history.ts | 2 +- .../src/server/api/endpoints/i/unpin.ts | 6 +- .../server/api/endpoints/i/update-email.ts | 20 +- .../src/server/api/endpoints/i/update.ts | 112 +- .../api/endpoints/i/user-group-invites.ts | 2 +- .../server/api/endpoints/i/webhooks/create.ts | 10 +- .../server/api/endpoints/i/webhooks/delete.ts | 2 +- .../server/api/endpoints/i/webhooks/list.ts | 4 +- .../server/api/endpoints/i/webhooks/show.ts | 4 +- .../server/api/endpoints/i/webhooks/update.ts | 8 +- .../src/server/api/endpoints/invite/create.ts | 6 +- .../src/server/api/endpoints/invite/delete.ts | 2 +- .../src/server/api/endpoints/invite/limit.ts | 2 +- .../src/server/api/endpoints/invite/list.ts | 2 +- .../server/api/endpoints/messaging/history.ts | 2 +- .../api/endpoints/messaging/messages.ts | 11 +- .../endpoints/messaging/messages/create.ts | 2 +- .../endpoints/messaging/messages/delete.ts | 2 +- .../api/endpoints/messaging/messages/read.ts | 2 +- .../backend/src/server/api/endpoints/meta.ts | 403 +- .../server/api/endpoints/miauth/gen-token.ts | 2 +- .../src/server/api/endpoints/mute/create.ts | 4 +- .../src/server/api/endpoints/mute/delete.ts | 2 +- .../src/server/api/endpoints/mute/list.ts | 2 +- .../src/server/api/endpoints/my/apps.ts | 2 +- .../backend/src/server/api/endpoints/notes.ts | 2 +- .../server/api/endpoints/notes/children.ts | 2 +- .../src/server/api/endpoints/notes/clips.ts | 2 +- .../api/endpoints/notes/conversation.ts | 2 +- .../server/api/endpoints/notes/create.test.ts | 16 +- .../src/server/api/endpoints/notes/create.ts | 172 +- .../src/server/api/endpoints/notes/delete.ts | 2 +- .../api/endpoints/notes/events/search.ts | 2 +- .../api/endpoints/notes/favorites/create.ts | 4 +- .../api/endpoints/notes/favorites/delete.ts | 2 +- .../server/api/endpoints/notes/featured.ts | 2 +- .../api/endpoints/notes/global-timeline.ts | 7 +- .../api/endpoints/notes/hybrid-timeline.ts | 28 +- .../api/endpoints/notes/local-timeline.ts | 14 +- .../server/api/endpoints/notes/mentions.ts | 8 +- .../endpoints/notes/polls/recommendation.ts | 9 +- .../server/api/endpoints/notes/polls/vote.ts | 6 +- .../server/api/endpoints/notes/reactions.ts | 4 +- .../api/endpoints/notes/reactions/create.ts | 9 +- .../api/endpoints/notes/reactions/delete.ts | 2 +- .../src/server/api/endpoints/notes/renotes.ts | 2 +- .../src/server/api/endpoints/notes/replies.ts | 2 +- .../api/endpoints/notes/search-by-tag.ts | 6 +- .../src/server/api/endpoints/notes/search.ts | 10 +- .../src/server/api/endpoints/notes/show.ts | 2 +- .../src/server/api/endpoints/notes/state.ts | 2 +- .../endpoints/notes/thread-muting/create.ts | 2 +- .../endpoints/notes/thread-muting/delete.ts | 2 +- .../server/api/endpoints/notes/timeline.ts | 28 +- .../server/api/endpoints/notes/translate.ts | 41 +- .../server/api/endpoints/notes/unrenote.ts | 2 +- .../src/server/api/endpoints/notes/update.ts | 9 +- .../api/endpoints/notes/user-list-timeline.ts | 2 +- .../api/endpoints/notifications/create.ts | 2 +- .../api/endpoints/notifications/delete.ts | 48 - .../api/endpoints/notifications/flush.ts | 33 - .../notifications/mark-all-as-read.ts | 2 +- .../notifications/test-notification.ts | 2 +- .../src/server/api/endpoints/page-push.ts | 4 +- .../src/server/api/endpoints/pages/create.ts | 6 +- .../src/server/api/endpoints/pages/delete.ts | 2 +- .../server/api/endpoints/pages/featured.ts | 2 +- .../src/server/api/endpoints/pages/like.ts | 4 +- .../src/server/api/endpoints/pages/show.ts | 2 +- .../src/server/api/endpoints/pages/unlike.ts | 2 +- .../src/server/api/endpoints/pages/update.ts | 25 +- .../backend/src/server/api/endpoints/ping.ts | 2 +- .../src/server/api/endpoints/pinned-users.ts | 4 +- .../src/server/api/endpoints/promo/read.ts | 4 +- .../api/endpoints/renote-mute/create.ts | 27 +- .../api/endpoints/renote-mute/delete.ts | 10 +- .../server/api/endpoints/renote-mute/list.ts | 2 +- .../api/endpoints/request-reset-password.ts | 2 +- .../src/server/api/endpoints/reset-db.ts | 2 +- .../server/api/endpoints/reset-password.ts | 7 +- .../src/server/api/endpoints/retention.ts | 28 +- .../src/server/api/endpoints/roles/list.ts | 2 +- .../src/server/api/endpoints/roles/notes.ts | 2 +- .../src/server/api/endpoints/roles/show.ts | 2 +- .../src/server/api/endpoints/roles/users.ts | 9 +- .../src/server/api/endpoints/server-info.ts | 2 +- .../backend/src/server/api/endpoints/stats.ts | 2 +- .../src/server/api/endpoints/sw/register.ts | 6 +- .../api/endpoints/sw/show-registration.ts | 2 +- .../src/server/api/endpoints/sw/unregister.ts | 9 +- .../api/endpoints/sw/update-registration.ts | 7 +- .../backend/src/server/api/endpoints/test.ts | 8 +- .../api/endpoints/username/available.ts | 2 +- .../backend/src/server/api/endpoints/users.ts | 4 +- .../api/endpoints/users/achievements.ts | 2 +- .../src/server/api/endpoints/users/clips.ts | 2 +- .../api/endpoints/users/featured-notes.ts | 2 +- .../src/server/api/endpoints/users/flashs.ts | 2 +- .../server/api/endpoints/users/followers.ts | 4 +- .../server/api/endpoints/users/following.ts | 11 +- .../api/endpoints/users/gallery/posts.ts | 2 +- .../users/get-frequently-replied-users.ts | 12 +- .../api/endpoints/users/groups/create.ts | 6 +- .../api/endpoints/users/groups/delete.ts | 2 +- .../users/groups/invitations/accept.ts | 2 +- .../users/groups/invitations/reject.ts | 2 +- .../api/endpoints/users/groups/invite.ts | 6 +- .../api/endpoints/users/groups/joined.ts | 2 +- .../api/endpoints/users/groups/leave.ts | 2 +- .../api/endpoints/users/groups/owned.ts | 2 +- .../server/api/endpoints/users/groups/pull.ts | 2 +- .../server/api/endpoints/users/groups/show.ts | 2 +- .../api/endpoints/users/groups/transfer.ts | 2 +- .../api/endpoints/users/groups/update.ts | 2 +- .../users/lists/create-from-public.ts | 14 +- .../api/endpoints/users/lists/create.ts | 8 +- .../api/endpoints/users/lists/delete.ts | 2 +- .../api/endpoints/users/lists/favorite.ts | 6 +- .../endpoints/users/lists/get-memberships.ts | 4 +- .../server/api/endpoints/users/lists/list.ts | 2 +- .../server/api/endpoints/users/lists/pull.ts | 2 +- .../server/api/endpoints/users/lists/push.ts | 6 +- .../server/api/endpoints/users/lists/show.ts | 4 +- .../api/endpoints/users/lists/unfavorite.ts | 4 +- .../users/lists/update-membership.ts | 2 +- .../api/endpoints/users/lists/update.ts | 2 +- .../src/server/api/endpoints/users/notes.ts | 11 +- .../src/server/api/endpoints/users/pages.ts | 2 +- .../server/api/endpoints/users/reactions.ts | 70 +- .../api/endpoints/users/recommendation.ts | 4 +- .../server/api/endpoints/users/relation.ts | 10 +- .../api/endpoints/users/report-abuse.ts | 32 +- .../users/search-by-username-and-host.ts | 103 +- .../src/server/api/endpoints/users/search.ts | 112 +- .../src/server/api/endpoints/users/show.ts | 14 +- .../src/server/api/endpoints/users/stats.ts | 2 +- .../server/api/endpoints/users/translate.ts | 28 +- .../server/api/endpoints/users/update-memo.ts | 2 +- packages/backend/src/server/api/error.ts | 2 +- .../api/openapi/OpenApiServerService.ts | 4 +- .../backend/src/server/api/openapi/errors.ts | 2 +- .../src/server/api/openapi/gen-spec.ts | 21 +- .../backend/src/server/api/openapi/schemas.ts | 93 +- .../src/server/api/stream/ChannelsService.ts | 3 +- .../src/server/api/stream/Connection.ts | 61 +- .../backend/src/server/api/stream/channel.ts | 37 +- .../src/server/api/stream/channels/admin.ts | 5 +- .../src/server/api/stream/channels/antenna.ts | 16 +- .../src/server/api/stream/channels/channel.ts | 23 +- .../src/server/api/stream/channels/drive.ts | 5 +- .../api/stream/channels/global-timeline.ts | 37 +- .../src/server/api/stream/channels/hashtag.ts | 20 +- .../api/stream/channels/home-timeline.ts | 40 +- .../api/stream/channels/hybrid-timeline.ts | 40 +- .../api/stream/channels/local-timeline.ts | 28 +- .../src/server/api/stream/channels/main.ts | 5 +- .../api/stream/channels/messaging-index.ts | 7 +- .../server/api/stream/channels/messaging.ts | 5 +- .../server/api/stream/channels/queue-stats.ts | 12 +- .../api/stream/channels/role-timeline.ts | 17 +- .../api/stream/channels/server-stats.ts | 10 +- .../server/api/stream/channels/user-list.ts | 29 +- .../src/server/oauth/OAuth2ProviderService.ts | 9 +- .../src/server/web/ClientLoggerService.ts | 2 +- .../src/server/web/ClientServerService.ts | 116 +- .../backend/src/server/web/FeedService.ts | 8 +- .../src/server/web/UrlPreviewService.ts | 71 +- packages/backend/src/server/web/bios.css | 37 +- packages/backend/src/server/web/bios.js | 2 +- packages/backend/src/server/web/boot.js | 19 +- packages/backend/src/server/web/cli.css | 37 +- packages/backend/src/server/web/cli.js | 2 +- packages/backend/src/server/web/error.css | 37 +- packages/backend/src/server/web/style.css | 2 +- .../backend/src/server/web/views/base.pug | 8 +- .../backend/src/server/web/views/note.pug | 4 +- .../backend/src/server/web/views/page.pug | 2 +- .../backend/src/server/web/views/user.pug | 2 +- packages/backend/src/types.ts | 75 +- packages/backend/test-server/.swcrc | 23 - packages/backend/test-server/entry.ts | 80 - packages/backend/test-server/eslint.config.js | 43 - packages/backend/test-server/tsconfig.json | 52 - packages/backend/test/.eslintrc.cjs | 11 + .../test/{compose.yml => docker-compose.yml} | 2 + packages/backend/test/e2e/2fa.ts | 133 +- packages/backend/test/e2e/antennas.ts | 152 +- packages/backend/test/e2e/api-visibility.ts | 113 +- packages/backend/test/e2e/api.ts | 68 +- packages/backend/test/e2e/block.ts | 47 +- packages/backend/test/e2e/clips.ts | 320 +- packages/backend/test/e2e/drive.ts | 85 - packages/backend/test/e2e/endpoints.ts | 388 +- packages/backend/test/e2e/exports.ts | 199 - packages/backend/test/e2e/fetch-resource.ts | 51 +- .../test/e2e/fetch-validate-ap-deny.ts | 40 - packages/backend/test/e2e/ff-visibility.ts | 222 +- packages/backend/test/e2e/move.ts | 285 +- packages/backend/test/e2e/mute.ts | 259 +- packages/backend/test/e2e/nodeinfo.ts | 15 +- packages/backend/test/e2e/note.ts | 596 +- packages/backend/test/e2e/oauth.ts | 38 +- packages/backend/test/e2e/renote-mute.ts | 70 +- packages/backend/test/e2e/streaming.ts | 216 +- .../backend/test/e2e/synalio/abuse-report.ts | 360 - .../backend/test/e2e/synalio/user-create.ts | 130 - packages/backend/test/e2e/thread-mute.ts | 46 +- packages/backend/test/e2e/timelines.ts | 909 +- packages/backend/test/e2e/user-notes.ts | 26 +- packages/backend/test/e2e/users.ts | 504 +- packages/backend/test/e2e/well-known.ts | 12 +- packages/backend/test/eslint.config.js | 22 - packages/backend/test/global.d.ts | 7 - packages/backend/test/jest.setup.ts | 13 - packages/backend/test/misc/mock-resolver.ts | 10 +- .../backend/test/prelude/get-api-validator.ts | 6 +- packages/backend/test/prelude/maybe.ts | 23 + packages/backend/test/prelude/url.ts | 2 +- packages/backend/test/resources/192.jpg | Bin 5131 -> 0 bytes packages/backend/test/resources/192.png | Bin 26568 -> 0 bytes packages/backend/test/resources/Lenna.jpg | Bin 0 -> 25360 bytes packages/backend/test/resources/Lenna.png | Bin 0 -> 473831 bytes .../backend/test/resources/kick_gaba7.m4a | Bin 9817 -> 0 bytes packages/backend/test/tsconfig.json | 3 +- .../unit/AbuseReportNotificationService.ts | 343 - .../backend/test/unit/AnnouncementService.ts | 14 +- packages/backend/test/unit/ApMfmService.ts | 49 - packages/backend/test/unit/DriveService.ts | 10 +- .../test/unit/FetchInstanceMetadataService.ts | 45 +- packages/backend/test/unit/FileInfoService.ts | 132 +- packages/backend/test/unit/MetaService.ts | 6 +- packages/backend/test/unit/MfmService.ts | 14 +- .../backend/test/unit/NoteCreateService.ts | 144 - packages/backend/test/unit/ReactionService.ts | 43 +- packages/backend/test/unit/RelayService.ts | 5 +- packages/backend/test/unit/RoleService.ts | 619 +- packages/backend/test/unit/S3Service.ts | 10 +- .../backend/test/unit/SystemWebhookService.ts | 516 - .../backend/test/unit/UserSearchService.ts | 265 - packages/backend/test/unit/activitypub.ts | 73 +- packages/backend/test/unit/ap-request.ts | 2 +- packages/backend/test/unit/chart.ts | 2 +- .../test/unit/entities/UserEntityService.ts | 528 - .../backend/test/unit/extract-mentions.ts | 2 +- .../backend/test/unit/misc/check-word-mute.ts | 2 +- .../test/unit/misc/correct-filename.ts | 2 +- packages/backend/test/unit/misc/id.ts | 6 +- packages/backend/test/unit/misc/is-renote.ts | 88 - packages/backend/test/unit/misc/loader.ts | 5 - packages/backend/test/unit/misc/others.ts | 4 +- packages/backend/test/utils.ts | 269 +- packages/backend/{scripts => }/watch.mjs | 2 +- packages/cherrypick-js/.eslintignore | 7 + packages/cherrypick-js/.eslintrc.cjs | 9 + packages/cherrypick-js/LICENSE | 2 +- packages/cherrypick-js/README.md | 2 +- packages/cherrypick-js/biome.json | 128 - packages/cherrypick-js/build.js | 104 - packages/cherrypick-js/eslint.config.js | 29 - .../cherrypick-js/etc/cherrypick-js.api.md | 1418 +- .../cherrypick-js/generator/.eslintrc.cjs | 9 + .../cherrypick-js/generator/eslint.config.js | 17 - packages/cherrypick-js/generator/package.json | 13 +- .../cherrypick-js/generator/src/generator.ts | 153 +- packages/cherrypick-js/jest.config.cjs | 12 +- packages/cherrypick-js/package.json | 53 +- packages/cherrypick-js/src/acct.ts | 3 +- packages/cherrypick-js/src/api.ts | 62 +- packages/cherrypick-js/src/api.types.ts | 42 +- .../src/autogen/apiClientJSDoc.ts | 229 +- .../cherrypick-js/src/autogen/endpoint.ts | 474 +- .../cherrypick-js/src/autogen/entities.ts | 1128 +- packages/cherrypick-js/src/autogen/models.ts | 21 +- packages/cherrypick-js/src/autogen/types.ts | 5748 +-- packages/cherrypick-js/src/consts.ts | 118 +- packages/cherrypick-js/src/entities.ts | 83 +- packages/cherrypick-js/src/index.ts | 15 +- packages/cherrypick-js/src/streaming.ts | 45 +- packages/cherrypick-js/src/streaming.types.ts | 24 +- packages/cherrypick-js/test-d/api.ts | 2 +- packages/cherrypick-js/test-d/streaming.ts | 2 +- packages/cherrypick-js/test/api.ts | 94 +- packages/cherrypick-js/test/streaming.ts | 2 +- packages/cherrypick-js/tsconfig.json | 3 +- packages/frontend/.eslintrc.cjs | 84 + packages/frontend/.storybook/changes.ts | 5 +- packages/frontend/.storybook/charts.ts | 48 - packages/frontend/.storybook/fakes.ts | 127 +- packages/frontend/.storybook/generate.tsx | 38 +- packages/frontend/.storybook/main.ts | 39 +- packages/frontend/.storybook/manager.ts | 2 +- packages/frontend/.storybook/mocks.ts | 37 +- .../frontend/.storybook/preload-locale.ts | 2 +- packages/frontend/.storybook/preload-theme.ts | 2 +- .../frontend/.storybook/preview-head.html | 7 +- packages/frontend/.storybook/preview.ts | 14 +- packages/frontend/@types/global.d.ts | 7 +- packages/frontend/@types/theme.d.ts | 2 +- .../frontend/assets/drop-and-fusion/bgm_1.mp3 | Bin 1637271 -> 0 bytes .../frontend/assets/drop-and-fusion/click.mp3 | Bin 26496 -> 0 bytes .../assets/drop-and-fusion/collision.mp3 | Bin 18240 -> 0 bytes .../assets/drop-and-fusion/collision_yen.mp3 | Bin 7807 -> 0 bytes .../assets/drop-and-fusion/drop-arrow.svg | 6 - .../frontend/assets/drop-and-fusion/drop.mp3 | Bin 18240 -> 0 bytes .../assets/drop-and-fusion/drop_yen.mp3 | Bin 5850 -> 0 bytes .../assets/drop-and-fusion/dropper.png | Bin 32415 -> 0 bytes .../assets/drop-and-fusion/frame-dark.svg | 28 - .../assets/drop-and-fusion/frame-light.svg | 28 - .../assets/drop-and-fusion/fusion.mp3 | Bin 19328 -> 0 bytes .../assets/drop-and-fusion/fusion_yen.mp3 | Bin 7807 -> 0 bytes .../assets/drop-and-fusion/gameover.mp3 | Bin 31346 -> 0 bytes .../assets/drop-and-fusion/gameover.png | Bin 67156 -> 0 bytes .../assets/drop-and-fusion/gameover_yen.mp3 | Bin 46392 -> 0 bytes .../frontend/assets/drop-and-fusion/go.png | Bin 31115 -> 0 bytes .../frontend/assets/drop-and-fusion/hold.mp3 | Bin 21941 -> 0 bytes .../frontend/assets/drop-and-fusion/logo.png | Bin 254016 -> 0 bytes .../normal_monos/cold_face.png | Bin 40776 -> 0 bytes .../normal_monos/exploding_head.png | Bin 47230 -> 0 bytes .../normal_monos/face_with_open_mouth.png | Bin 36399 -> 0 bytes .../face_with_symbols_on_mouth.png | Bin 40322 -> 0 bytes .../normal_monos/grinning_squinting_face.png | Bin 41020 -> 0 bytes .../normal_monos/heart_suit.png | Bin 22437 -> 0 bytes .../normal_monos/pleading_face.png | Bin 44074 -> 0 bytes .../normal_monos/smiling_face_with_hearts.png | Bin 52432 -> 0 bytes .../smiling_face_with_sunglasses.png | Bin 47859 -> 0 bytes .../normal_monos/zany_face.png | Bin 44995 -> 0 bytes .../frontend/assets/drop-and-fusion/ready.png | Bin 34674 -> 0 bytes .../drop-and-fusion/square_monos/keycap_1.png | Bin 29193 -> 0 bytes .../square_monos/keycap_10.png | Bin 33717 -> 0 bytes .../drop-and-fusion/square_monos/keycap_2.png | Bin 32324 -> 0 bytes .../drop-and-fusion/square_monos/keycap_3.png | Bin 33127 -> 0 bytes .../drop-and-fusion/square_monos/keycap_4.png | Bin 31182 -> 0 bytes .../drop-and-fusion/square_monos/keycap_5.png | Bin 32745 -> 0 bytes .../drop-and-fusion/square_monos/keycap_6.png | Bin 32100 -> 0 bytes .../drop-and-fusion/square_monos/keycap_7.png | Bin 31318 -> 0 bytes .../drop-and-fusion/square_monos/keycap_8.png | Bin 32886 -> 0 bytes .../drop-and-fusion/square_monos/keycap_9.png | Bin 32483 -> 0 bytes .../sweets_monos/candy_color.svg | 86 - .../sweets_monos/chocolate_bar_color.svg | 316 - .../sweets_monos/cookie_color.svg | 116 - .../sweets_monos/custard_color.svg | 22 - .../sweets_monos/doughnut_color.svg | 272 - .../sweets_monos/lollipop_color.svg | 112 - .../sweets_monos/pancakes_color.svg | 92 - .../sweets_monos/shaved_ice_color.svg | 161 - .../sweets_monos/shortcake_color.svg | 48 - .../sweets_monos/soft_ice_cream_color.svg | 140 - .../sweets_monos/verts/candy_color.svg | 5 - .../verts/chocolate_bar_color.svg | 5 - .../sweets_monos/verts/custard_color.svg | 5 - .../sweets_monos/verts/doughnut_color.svg | 5 - .../sweets_monos/verts/lollipop_color.svg | 5 - .../sweets_monos/verts/pancakes_color.svg | 5 - .../sweets_monos/verts/shaved_ice_color.svg | 5 - .../sweets_monos/verts/shortcake_color.svg | 5 - .../verts/soft_ice_cream_color.svg | 5 - .../drop-and-fusion/yen_monos/10000yen.png | Bin 93558 -> 0 bytes .../drop-and-fusion/yen_monos/1000yen.png | Bin 98316 -> 0 bytes .../drop-and-fusion/yen_monos/100yen.png | Bin 55276 -> 0 bytes .../drop-and-fusion/yen_monos/10yen.png | Bin 67485 -> 0 bytes .../assets/drop-and-fusion/yen_monos/1yen.png | Bin 57534 -> 0 bytes .../drop-and-fusion/yen_monos/2000yen.png | Bin 88045 -> 0 bytes .../drop-and-fusion/yen_monos/5000yen.png | Bin 94334 -> 0 bytes .../drop-and-fusion/yen_monos/500yen.png | Bin 67547 -> 0 bytes .../drop-and-fusion/yen_monos/50yen.png | Bin 41915 -> 0 bytes .../assets/drop-and-fusion/yen_monos/5yen.png | Bin 60516 -> 0 bytes packages/frontend/assets/reversi/logo.png | Bin 185387 -> 0 bytes packages/frontend/assets/reversi/lose.mp3 | Bin 9565 -> 0 bytes packages/frontend/assets/reversi/matched.mp3 | Bin 43884 -> 0 bytes packages/frontend/assets/reversi/put.mp3 | Bin 5014 -> 0 bytes packages/frontend/assets/reversi/stone_b.png | Bin 10794 -> 0 bytes packages/frontend/assets/reversi/stone_w.png | Bin 11546 -> 0 bytes packages/frontend/assets/reversi/win.mp3 | Bin 25703 -> 0 bytes .../frontend/assets/sounds/mujin/bubble.mp3 | Bin 16896 -> 0 bytes .../assets/sounds/mujin/notification_1.mp3 | Bin 50304 -> 0 bytes .../assets/sounds/mujin/test_notification.mp3 | Bin 33024 -> 0 bytes packages/frontend/biome.json | 12 - packages/frontend/eslint.config.js | 96 - ...lugin-unwind-css-module-class-name.test.ts | 2 +- ...lup-plugin-unwind-css-module-class-name.ts | 2 +- packages/frontend/package.json | 173 +- packages/frontend/public/mockServiceWorker.js | 2 +- packages/frontend/src/_boot_.ts | 2 +- packages/frontend/src/_dev_boot_.ts | 4 +- packages/frontend/src/account.ts | 34 +- packages/frontend/src/boot/common.ts | 19 +- packages/frontend/src/boot/main-boot.ts | 149 +- packages/frontend/src/boot/sub-boot.ts | 5 +- packages/frontend/src/cache.ts | 13 +- .../components/MkAbuseReport.stories.impl.ts | 10 +- .../frontend/src/components/MkAbuseReport.vue | 8 +- .../MkAbuseReportResolver.stories.impl.ts | 2 +- .../src/components/MkAbuseReportResolver.vue | 2 +- .../MkAbuseReportWindow.stories.impl.ts | 10 +- .../src/components/MkAbuseReportWindow.vue | 4 +- .../components/MkAccountMoved.stories.impl.ts | 17 +- .../src/components/MkAccountMoved.vue | 6 +- .../components/MkAchievements.stories.impl.ts | 12 +- .../src/components/MkAchievements.vue | 12 +- .../components/MkAnalogClock.stories.impl.ts | 2 +- .../frontend/src/components/MkAnalogClock.vue | 2 +- packages/frontend/src/components/MkAnimBg.vue | 2 +- .../MkAnnouncementDialog.stories.impl.ts | 22 +- .../src/components/MkAnnouncementDialog.vue | 7 +- .../MkAntennaEditor.stories.impl.ts | 62 - .../MkAntennaEditorDialog.stories.impl.ts | 63 - .../src/components/MkAntennaEditorDialog.vue | 63 - .../src/components/MkAsUi.stories.impl.ts | 2 +- packages/frontend/src/components/MkAsUi.vue | 48 +- .../components/MkAutocomplete.stories.impl.ts | 19 +- .../src/components/MkAutocomplete.vue | 117 +- .../src/components/MkAvatars.stories.impl.ts | 10 +- .../frontend/src/components/MkAvatars.vue | 6 +- .../src/components/MkButton.stories.impl.ts | 2 +- packages/frontend/src/components/MkButton.vue | 13 +- .../src/components/MkCaptcha.stories.impl.ts | 2 +- .../frontend/src/components/MkCaptcha.vue | 45 +- .../MkChannelFollowButton.stories.impl.ts | 71 - .../src/components/MkChannelFollowButton.vue | 25 +- .../components/MkChannelList.stories.impl.ts | 65 - .../frontend/src/components/MkChannelList.vue | 2 +- .../MkChannelPreview.stories.impl.ts | 43 - .../src/components/MkChannelPreview.vue | 21 +- .../src/components/MkChart.stories.impl.ts | 80 - packages/frontend/src/components/MkChart.vue | 170 +- .../components/MkChartLegend.stories.impl.ts | 7 - .../frontend/src/components/MkChartLegend.vue | 16 +- .../components/MkChartTooltip.stories.impl.ts | 7 - .../src/components/MkChartTooltip.vue | 2 +- .../frontend/src/components/MkChatPreview.vue | 6 +- .../components/MkClickerGame.stories.impl.ts | 77 - .../frontend/src/components/MkClickerGame.vue | 8 +- .../components/MkClipPreview.stories.impl.ts | 44 - .../frontend/src/components/MkClipPreview.vue | 69 +- .../components/MkCode.core.stories.impl.ts | 7 - .../frontend/src/components/MkCode.core.vue | 80 +- .../src/components/MkCode.stories.impl.ts | 44 - packages/frontend/src/components/MkCode.vue | 71 +- .../components/MkCodeEditor.stories.impl.ts | 62 - .../frontend/src/components/MkCodeEditor.vue | 22 +- .../components/MkCodeInline.stories.impl.ts | 37 - .../frontend/src/components/MkCodeInline.vue | 25 - .../components/MkColorInput.stories.impl.ts | 50 - .../frontend/src/components/MkColorInput.vue | 6 +- .../components/MkContainer.stories.impl.ts | 7 - .../frontend/src/components/MkContainer.vue | 2 +- .../components/MkContextMenu.stories.impl.ts | 58 - .../frontend/src/components/MkContextMenu.vue | 22 +- .../MkCropperDialog.stories.impl.ts | 75 - .../src/components/MkCropperDialog.vue | 22 +- ...kCustomEmojiDetailedDialog.stories.impl.ts | 38 - .../MkCustomEmojiDetailedDialog.vue | 108 - .../src/components/MkCwButton.stories.impl.ts | 79 - .../frontend/src/components/MkCwButton.vue | 30 +- .../MkDateSeparatedList.stories.impl.ts | 7 - .../src/components/MkDateSeparatedList.vue | 46 +- .../src/components/MkDeprecatedWarning.vue | 22 - .../src/components/MkDialog.stories.impl.ts | 159 - packages/frontend/src/components/MkDialog.vue | 73 +- packages/frontend/src/components/MkDice.vue | 87 - .../components/MkDigitalClock.stories.impl.ts | 2 +- .../src/components/MkDigitalClock.vue | 2 +- .../src/components/MkDivider.stories.impl.ts | 7 - .../frontend/src/components/MkDivider.vue | 32 - .../src/components/MkDonation.stories.impl.ts | 54 - .../frontend/src/components/MkDonation.vue | 2 +- .../components/MkDrive.file.stories.impl.ts | 48 - .../frontend/src/components/MkDrive.file.vue | 36 +- .../components/MkDrive.folder.stories.impl.ts | 70 - .../src/components/MkDrive.folder.vue | 94 +- .../MkDrive.navFolder.stories.impl.ts | 7 - .../src/components/MkDrive.navFolder.vue | 8 +- .../src/components/MkDrive.stories.impl.ts | 82 - packages/frontend/src/components/MkDrive.vue | 44 +- .../MkDriveFileThumbnail.stories.impl.ts | 41 - .../src/components/MkDriveFileThumbnail.vue | 11 +- .../MkDriveSelectDialog.stories.impl.ts | 7 - .../src/components/MkDriveSelectDialog.vue | 10 +- .../components/MkDriveWindow.stories.impl.ts | 7 - .../frontend/src/components/MkDriveWindow.vue | 2 +- .../MkEmojiPicker.section.stories.impl.ts | 7 - .../src/components/MkEmojiPicker.section.vue | 15 +- .../components/MkEmojiPicker.stories.impl.ts | 54 - .../frontend/src/components/MkEmojiPicker.vue | 151 +- .../MkEmojiPickerDialog.stories.impl.ts | 7 - .../src/components/MkEmojiPickerDialog.vue | 12 +- .../src/components/MkEmojiPickerWindow.vue | 47 + .../src/components/MkEvent.stories.impl.ts | 2 +- packages/frontend/src/components/MkEvent.vue | 2 +- .../frontend/src/components/MkEventEditor.vue | 3 +- .../src/components/MkFeaturedPhotos.vue | 14 +- .../components/MkFileCaptionEditWindow.vue | 6 +- .../src/components/MkFileListForAdmin.vue | 4 +- .../src/components/MkFlashPreview.vue | 18 +- .../components/MkFlashRequestTokenDialog.vue | 4 +- .../src/components/MkFoldableSection.vue | 53 +- packages/frontend/src/components/MkFolder.vue | 22 +- .../src/components/MkFollowButton.vue | 49 +- .../src/components/MkForgotPassword.vue | 6 +- .../src/components/MkFormDialog.file.vue | 71 - .../frontend/src/components/MkFormDialog.vue | 78 +- .../MkGalleryPostPreview.stories.impl.ts | 5 +- .../src/components/MkGalleryPostPreview.vue | 10 +- packages/frontend/src/components/MkGoogle.vue | 2 +- .../frontend/src/components/MkHeatmap.vue | 69 +- .../src/components/MkHorizontalSwipe.vue | 239 - .../src/components/MkImgWithBlurhash.vue | 38 +- packages/frontend/src/components/MkInfo.vue | 4 +- packages/frontend/src/components/MkInput.vue | 37 +- .../MkInstanceCardMini.stories.impl.ts | 65 - .../src/components/MkInstanceCardMini.vue | 10 +- .../src/components/MkInstanceStats.vue | 45 +- .../src/components/MkInstanceTicker.vue | 10 +- .../components/MkInviteCode.stories.impl.ts | 8 +- .../frontend/src/components/MkInviteCode.vue | 4 +- .../frontend/src/components/MkKeyValue.vue | 4 +- .../frontend/src/components/MkLaunchPad.vue | 9 +- packages/frontend/src/components/MkLink.vue | 26 +- .../frontend/src/components/MkMarquee.vue | 3 +- .../frontend/src/components/MkMediaAudio.vue | 523 - .../frontend/src/components/MkMediaBanner.vue | 39 +- .../frontend/src/components/MkMediaImage.vue | 53 +- .../frontend/src/components/MkMediaList.vue | 128 +- .../frontend/src/components/MkMediaRange.vue | 152 - .../frontend/src/components/MkMediaVideo.vue | 701 +- .../frontend/src/components/MkMention.vue | 7 +- .../frontend/src/components/MkMenu.child.vue | 12 +- packages/frontend/src/components/MkMenu.vue | 446 +- .../src/components/MkMfmCheatSheetDialog.vue | 5 - .../frontend/src/components/MkMigrated.vue | 4 +- .../frontend/src/components/MkMiniChart.vue | 6 +- packages/frontend/src/components/MkModal.vue | 69 +- .../frontend/src/components/MkModalWindow.vue | 35 +- packages/frontend/src/components/MkNote.vue | 367 +- .../src/components/MkNoteDetailed.vue | 271 +- .../frontend/src/components/MkNoteHeader.vue | 11 +- .../frontend/src/components/MkNotePreview.vue | 14 +- .../frontend/src/components/MkNoteSimple.vue | 16 +- .../frontend/src/components/MkNoteSub.vue | 17 +- packages/frontend/src/components/MkNotes.vue | 2 +- .../src/components/MkNotification.vue | 91 +- .../components/MkNotificationSelectWindow.vue | 4 +- .../src/components/MkNotifications.vue | 15 +- packages/frontend/src/components/MkNumber.vue | 23 +- .../frontend/src/components/MkNumberDiff.vue | 2 +- .../src/components/MkObjectView.value.vue | 2 +- .../frontend/src/components/MkObjectView.vue | 2 +- packages/frontend/src/components/MkOmit.vue | 6 +- .../frontend/src/components/MkPagePreview.vue | 23 +- .../frontend/src/components/MkPageWindow.vue | 70 +- .../frontend/src/components/MkPagination.vue | 10 +- .../src/components/MkPasswordDialog.vue | 32 +- .../src/components/MkPlusOneEffect.vue | 7 +- packages/frontend/src/components/MkPoll.vue | 56 +- .../frontend/src/components/MkPollEditor.vue | 48 +- .../frontend/src/components/MkPopupMenu.vue | 8 +- .../frontend/src/components/MkPostForm.vue | 347 +- .../src/components/MkPostFormAttaches.vue | 42 +- .../src/components/MkPostFormDialog.vue | 20 +- .../src/components/MkPostFormSimple.vue | 207 +- .../frontend/src/components/MkPreview.vue | 151 - .../src/components/MkPullToRefresh.vue | 9 +- .../src/components/MkPushNotification.vue | 6 +- .../MkPushNotificationAllowButton.vue | 11 +- packages/frontend/src/components/MkQrcode.vue | 102 - packages/frontend/src/components/MkRadio.vue | 12 +- packages/frontend/src/components/MkRadios.vue | 11 +- packages/frontend/src/components/MkRange.vue | 18 +- .../src/components/MkReactedUsersDialog.vue | 16 +- .../src/components/MkReactionEffect.vue | 2 +- .../src/components/MkReactionIcon.vue | 10 +- .../src/components/MkReactionTooltip.vue | 2 +- .../components/MkReactionsViewer.details.vue | 3 +- .../components/MkReactionsViewer.reaction.vue | 149 +- .../src/components/MkReactionsViewer.vue | 5 +- .../src/components/MkRemoteCaution.vue | 2 +- .../src/components/MkRenotedUsersDialog.vue | 11 +- .../src/components/MkRetentionHeatmap.vue | 36 +- .../src/components/MkRetentionLineChart.vue | 18 +- .../src/components/MkRippleEffect.vue | 11 +- .../frontend/src/components/MkRolePreview.vue | 2 +- .../src/components/MkScheduledNoteDelete.vue | 168 - .../frontend/src/components/MkSearchInput.vue | 298 - .../src/components/MkSearchResultWindow.vue | 35 - packages/frontend/src/components/MkSelect.vue | 71 +- packages/frontend/src/components/MkSignin.vue | 171 +- .../src/components/MkSigninDialog.vue | 11 +- .../src/components/MkSignupDialog.form.vue | 20 +- .../MkSignupDialog.rules.stories.impl.ts | 11 +- .../src/components/MkSignupDialog.rules.vue | 12 +- .../src/components/MkSignupDialog.vue | 16 +- .../components/MkSourceCodeAvailablePopup.vue | 113 - .../frontend/src/components/MkSparkle.vue | 9 +- .../src/components/MkSubNoteContent.vue | 164 +- .../frontend/src/components/MkSuperMenu.vue | 12 +- .../src/components/MkSwitch.button.vue | 17 +- packages/frontend/src/components/MkSwitch.vue | 17 +- .../components/MkSystemWebhookEditor.impl.ts | 46 - .../src/components/MkSystemWebhookEditor.vue | 238 - packages/frontend/src/components/MkTab.vue | 14 +- .../frontend/src/components/MkTagCloud.vue | 4 +- .../frontend/src/components/MkTextarea.vue | 22 +- .../frontend/src/components/MkTimeline.vue | 85 +- packages/frontend/src/components/MkToast.vue | 2 +- .../src/components/MkTokenGenerateWindow.vue | 70 +- .../frontend/src/components/MkTooltip.vue | 9 +- .../src/components/MkTutorialDialog.Note.vue | 8 +- .../components/MkTutorialDialog.PostNote.vue | 7 +- .../components/MkTutorialDialog.Sensitive.vue | 7 +- .../components/MkTutorialDialog.Timeline.vue | 12 +- .../src/components/MkTutorialDialog.vue | 6 +- .../frontend/src/components/MkUpdated.vue | 10 +- .../frontend/src/components/MkUrlPreview.vue | 42 +- .../src/components/MkUrlPreviewPopup.vue | 6 +- .../MkUserAnnouncementEditDialog.vue | 39 +- .../src/components/MkUserCardMini.vue | 110 +- .../frontend/src/components/MkUserInfo.vue | 8 +- .../frontend/src/components/MkUserList.vue | 2 +- .../src/components/MkUserOnlineIndicator.vue | 2 +- .../frontend/src/components/MkUserPopup.vue | 13 +- .../src/components/MkUserSelectDialog.vue | 112 +- .../src/components/MkUserSensitiveCaution.vue | 19 - .../MkUserSetupDialog.Blur.stories.impl.ts | 2 +- .../src/components/MkUserSetupDialog.Blur.vue | 2 +- .../MkUserSetupDialog.Follow.stories.impl.ts | 16 +- .../components/MkUserSetupDialog.Follow.vue | 30 +- ...MkUserSetupDialog.FontSize.stories.impl.ts | 2 +- .../components/MkUserSetupDialog.FontSize.vue | 2 +- ...og.MisskeyFlavoredMarkdown.stories.impl.ts | 2 +- ...serSetupDialog.MisskeyFlavoredMarkdown.vue | 2 +- .../MkUserSetupDialog.Privacy.stories.impl.ts | 2 +- .../components/MkUserSetupDialog.Privacy.vue | 6 +- .../MkUserSetupDialog.Profile.stories.impl.ts | 2 +- .../components/MkUserSetupDialog.Profile.vue | 8 +- .../MkUserSetupDialog.User.stories.impl.ts | 2 +- .../src/components/MkUserSetupDialog.User.vue | 6 +- .../MkUserSetupDialog.stories.impl.ts | 16 +- .../src/components/MkUserSetupDialog.vue | 14 +- .../src/components/MkUsersTooltip.vue | 2 +- .../src/components/MkVisibilityPicker.vue | 32 +- .../MkVisitorDashboard.ActiveUsersChart.vue | 23 +- .../src/components/MkVisitorDashboard.vue | 79 +- .../src/components/MkWaitingDialog.vue | 4 +- .../src/components/MkWelcomeToast.vue | 5 - .../frontend/src/components/MkWidgets.vue | 23 +- packages/frontend/src/components/MkWindow.vue | 83 +- .../src/components/MkYouTubePlayer.vue | 7 +- .../frontend/src/components/form/link.vue | 2 +- .../frontend/src/components/form/section.vue | 2 +- .../frontend/src/components/form/slot.vue | 2 +- .../frontend/src/components/form/split.vue | 2 +- .../frontend/src/components/form/suspense.vue | 2 +- .../components/global/CPAvatar-Friendly.vue | 20 +- .../src/components/global/CPPageHeader.vue | 56 +- .../frontend/src/components/global/I18n.vue | 51 - .../src/components/global/MkA.stories.impl.ts | 11 +- .../frontend/src/components/global/MkA.vue | 30 +- .../components/global/MkAcct.stories.impl.ts | 2 +- .../frontend/src/components/global/MkAcct.vue | 4 +- .../components/global/MkAd.stories.impl.ts | 93 +- .../frontend/src/components/global/MkAd.vue | 23 +- .../global/MkAvatar.stories.impl.ts | 5 +- .../src/components/global/MkAvatar.vue | 67 +- .../global/MkCondensedLine.stories.impl.ts | 4 +- .../src/components/global/MkCondensedLine.vue | 2 +- .../global/MkCustomEmoji.stories.impl.ts | 17 +- .../src/components/global/MkCustomEmoji.vue | 33 +- .../global/MkEllipsis.stories.impl.ts | 2 +- .../src/components/global/MkEllipsis.vue | 2 +- .../components/global/MkEmoji.stories.impl.ts | 2 +- .../src/components/global/MkEmoji.vue | 21 +- .../components/global/MkError.stories.impl.ts | 5 +- .../components/global/MkError.stories.meta.ts | 9 +- .../src/components/global/MkError.vue | 2 +- .../src/components/global/MkFooterSpacer.vue | 2 +- .../frontend/src/components/global/MkLazy.vue | 2 +- .../global/MkLoading.stories.impl.ts | 2 +- .../src/components/global/MkLoading.vue | 2 +- .../MkMisskeyFlavoredMarkdown.stories.impl.ts | 5 +- .../global/MkMisskeyFlavoredMarkdown.ts | 71 +- .../global/MkPageHeader.stories.impl.ts | 9 +- .../global/MkPageHeader.tabs.stories.impl.ts | 2 +- .../components/global/MkPageHeader.tabs.vue | 13 +- .../src/components/global/MkPageHeader.vue | 42 +- .../src/components/global/MkSpacer.vue | 2 +- .../global/MkStickyContainer.stories.impl.ts | 2 +- .../components/global/MkStickyContainer.vue | 33 +- .../components/global/MkTime.stories.impl.ts | 33 +- .../frontend/src/components/global/MkTime.vue | 56 +- .../components/global/MkUrl.stories.impl.ts | 13 +- .../frontend/src/components/global/MkUrl.vue | 16 +- .../global/MkUserName.stories.impl.ts | 6 +- .../src/components/global/MkUserName.vue | 2 +- .../global/NotificationPageHeader.vue | 402 - .../global/RouterView.stories.impl.ts | 2 +- .../src/components/global/RouterView.vue | 50 +- .../src/components/global/ToastAvatar.vue | 28 +- .../frontend/src/components/global/i18n.ts | 29 + packages/frontend/src/components/index.ts | 4 +- .../src/components/page/block.type.ts | 34 + .../src/components/page/page.block.vue | 20 +- .../src/components/page/page.dynamic.vue | 43 - .../src/components/page/page.image.vue | 36 +- .../src/components/page/page.note.vue | 25 +- .../src/components/page/page.section.vue | 30 +- .../src/components/page/page.text.vue | 20 +- .../frontend/src/components/page/page.vue | 4 +- packages/frontend/src/config.ts | 4 +- packages/frontend/src/const.ts | 35 +- packages/frontend/src/custom-emojis.ts | 8 +- packages/frontend/src/debug.ts | 2 +- .../frontend/src/directives/adaptive-bg.ts | 2 +- .../src/directives/adaptive-border.ts | 2 +- packages/frontend/src/directives/anim.ts | 2 +- packages/frontend/src/directives/appear.ts | 2 +- .../frontend/src/directives/click-anime.ts | 2 +- .../frontend/src/directives/follow-append.ts | 2 +- packages/frontend/src/directives/get-size.ts | 2 +- packages/frontend/src/directives/hotkey.ts | 8 +- packages/frontend/src/directives/index.ts | 2 +- packages/frontend/src/directives/panel.ts | 2 +- packages/frontend/src/directives/ripple.ts | 6 +- packages/frontend/src/directives/tooltip.ts | 8 +- .../frontend/src/directives/user-preview.ts | 8 +- packages/frontend/src/events.ts | 10 +- .../frontend/src/filters/bytes-net-sizes.ts | 2 +- packages/frontend/src/filters/bytes-net-v.ts | 2 +- packages/frontend/src/filters/bytes.ts | 6 +- packages/frontend/src/filters/date.ts | 2 +- packages/frontend/src/filters/hms.ts | 65 - packages/frontend/src/filters/kmg.ts | 14 - packages/frontend/src/filters/note.ts | 2 +- packages/frontend/src/filters/number.ts | 2 +- packages/frontend/src/filters/user.ts | 4 +- packages/frontend/src/i18n.ts | 6 +- packages/frontend/src/index.html | 11 +- packages/frontend/src/instance.ts | 41 +- packages/frontend/src/local-storage.ts | 4 +- packages/frontend/src/navbar.ts | 9 +- packages/frontend/src/nirax.ts | 236 +- packages/frontend/src/os.ts | 529 +- packages/frontend/src/pages/_empty_.vue | 2 +- packages/frontend/src/pages/_error_.vue | 13 +- packages/frontend/src/pages/_loading_.vue | 2 +- packages/frontend/src/pages/about-misskey.vue | 145 +- packages/frontend/src/pages/about.emojis.vue | 2 +- .../frontend/src/pages/about.federation.vue | 15 +- .../frontend/src/pages/about.overview.vue | 209 - packages/frontend/src/pages/about.vue | 203 +- packages/frontend/src/pages/achievements.vue | 6 +- packages/frontend/src/pages/admin-file.vue | 17 +- packages/frontend/src/pages/admin-user.vue | 69 +- .../src/pages/admin/RolesEditorFormula.vue | 16 +- .../frontend/src/pages/admin/_header_.vue | 22 +- .../notification-recipient.editor.vue | 321 - .../notification-recipient.item.vue | 114 - .../abuse-report/notification-recipient.vue | 177 - packages/frontend/src/pages/admin/abuses.vue | 107 +- packages/frontend/src/pages/admin/ads.vue | 19 +- .../src/pages/admin/announcements.vue | 173 +- .../src/pages/admin/bot-protection.vue | 41 +- .../frontend/src/pages/admin/branding.vue | 41 +- .../frontend/src/pages/admin/database.vue | 10 +- .../src/pages/admin/email-settings.vue | 11 +- .../src/pages/admin/external-services.vue | 11 +- .../frontend/src/pages/admin/federation.vue | 26 +- packages/frontend/src/pages/admin/files.vue | 32 +- packages/frontend/src/pages/admin/index.vue | 69 +- .../src/pages/admin/instance-block.vue | 38 +- packages/frontend/src/pages/admin/invites.vue | 11 +- .../frontend/src/pages/admin/moderation.vue | 28 +- .../src/pages/admin/modlog.ModLog.vue | 56 +- packages/frontend/src/pages/admin/modlog.vue | 8 +- .../src/pages/admin/object-storage.vue | 11 +- .../src/pages/admin/other-settings.vue | 11 +- .../src/pages/admin/overview.active-users.vue | 6 +- .../src/pages/admin/overview.ap-requests.vue | 6 +- .../src/pages/admin/overview.federation.vue | 7 +- .../src/pages/admin/overview.heatmap.vue | 2 +- .../src/pages/admin/overview.instances.vue | 6 +- .../src/pages/admin/overview.moderators.vue | 6 +- .../frontend/src/pages/admin/overview.pie.vue | 2 +- .../src/pages/admin/overview.queue.chart.vue | 2 +- .../src/pages/admin/overview.queue.vue | 2 +- .../src/pages/admin/overview.retention.vue | 2 +- .../src/pages/admin/overview.stats.vue | 12 +- .../src/pages/admin/overview.users.vue | 6 +- .../frontend/src/pages/admin/overview.vue | 19 +- .../src/pages/admin/proxy-account.vue | 15 +- .../src/pages/admin/queue.chart.chart.vue | 2 +- .../frontend/src/pages/admin/queue.chart.vue | 6 +- packages/frontend/src/pages/admin/queue.vue | 6 +- packages/frontend/src/pages/admin/relays.vue | 15 +- .../frontend/src/pages/admin/roles.edit.vue | 14 +- .../frontend/src/pages/admin/roles.editor.vue | 84 +- .../frontend/src/pages/admin/roles.role.vue | 21 +- packages/frontend/src/pages/admin/roles.vue | 46 +- .../frontend/src/pages/admin/security.vue | 59 +- .../frontend/src/pages/admin/server-rules.vue | 8 +- .../frontend/src/pages/admin/settings.vue | 115 +- .../src/pages/admin/system-webhook.item.vue | 117 - .../src/pages/admin/system-webhook.vue | 96 - packages/frontend/src/pages/admin/update.vue | 11 +- packages/frontend/src/pages/admin/users.vue | 10 +- packages/frontend/src/pages/ads.vue | 6 +- packages/frontend/src/pages/announcement.vue | 151 - packages/frontend/src/pages/announcements.vue | 81 +- .../frontend/src/pages/antenna-timeline.vue | 18 +- packages/frontend/src/pages/api-console.vue | 14 +- packages/frontend/src/pages/auth.form.vue | 14 +- packages/frontend/src/pages/auth.vue | 16 +- .../frontend/src/pages/avatar-decorations.vue | 13 +- .../frontend/src/pages/channel-editor.vue | 20 +- packages/frontend/src/pages/channel.vue | 103 +- packages/frontend/src/pages/channels.vue | 87 +- packages/frontend/src/pages/clicker.vue | 6 +- packages/frontend/src/pages/clip.vue | 28 +- packages/frontend/src/pages/contact.vue | 48 - .../src/pages/custom-emojis-manager.vue | 23 +- .../frontend/src/pages/drive.file.info.vue | 58 +- .../frontend/src/pages/drive.file.notes.vue | 2 +- packages/frontend/src/pages/drive.file.vue | 21 +- packages/frontend/src/pages/drive.vue | 6 +- .../src/pages/drop-and-fusion.game.vue | 1509 - .../frontend/src/pages/drop-and-fusion.vue | 160 - .../frontend/src/pages/emoji-edit-dialog.vue | 48 +- packages/frontend/src/pages/emojis.emoji.vue | 28 +- .../frontend/src/pages/explore.featured.vue | 5 +- packages/frontend/src/pages/explore.roles.vue | 6 +- packages/frontend/src/pages/explore.users.vue | 8 +- packages/frontend/src/pages/explore.vue | 17 +- packages/frontend/src/pages/favorites.vue | 6 +- .../frontend/src/pages/flash/flash-edit.vue | 80 +- .../frontend/src/pages/flash/flash-index.vue | 47 +- packages/frontend/src/pages/flash/flash.vue | 128 +- .../frontend/src/pages/follow-requests.vue | 12 +- packages/frontend/src/pages/follow.vue | 71 + packages/frontend/src/pages/gallery/edit.vue | 14 +- packages/frontend/src/pages/gallery/index.vue | 19 +- packages/frontend/src/pages/gallery/post.vue | 26 +- packages/frontend/src/pages/games.vue | 36 - ...-extensions.vue => install-extentions.vue} | 16 +- packages/frontend/src/pages/instance-info.vue | 271 +- packages/frontend/src/pages/invite.vue | 15 +- packages/frontend/src/pages/list.vue | 13 +- packages/frontend/src/pages/lookup.vue | 97 - .../frontend/src/pages/messaging/index.vue | 24 +- .../pages/messaging/messaging-room.form.vue | 20 +- .../messaging/messaging-room.message.vue | 7 +- .../src/pages/messaging/messaging-room.vue | 20 +- .../frontend/src/pages/mfm-cheat-sheet.vue | 17 +- packages/frontend/src/pages/miauth.vue | 16 +- .../frontend/src/pages/my-antennas/create.vue | 40 +- .../frontend/src/pages/my-antennas/edit.vue | 29 +- .../my-antennas/editor.vue} | 95 +- .../frontend/src/pages/my-antennas/index.vue | 8 +- .../frontend/src/pages/my-clips/index.vue | 37 +- .../frontend/src/pages/my-groups/group.vue | 23 +- .../frontend/src/pages/my-groups/index.vue | 9 +- .../frontend/src/pages/my-lists/index.vue | 12 +- packages/frontend/src/pages/my-lists/list.vue | 54 +- packages/frontend/src/pages/not-found.vue | 6 +- packages/frontend/src/pages/note.vue | 93 +- .../src/pages/notifications-friendly.vue | 121 - packages/frontend/src/pages/notifications.vue | 40 +- packages/frontend/src/pages/oauth.vue | 12 +- .../page-editor/els/page-editor.el.image.vue | 5 +- .../page-editor/els/page-editor.el.note.vue | 8 +- .../els/page-editor.el.section.vue | 2 +- .../page-editor/els/page-editor.el.text.vue | 2 +- .../pages/page-editor/page-editor.blocks.vue | 2 +- .../page-editor/page-editor.container.vue | 2 +- .../src/pages/page-editor/page-editor.vue | 37 +- packages/frontend/src/pages/page.vue | 425 +- packages/frontend/src/pages/pages.vue | 57 +- packages/frontend/src/pages/preview.vue | 26 - packages/frontend/src/pages/registry.keys.vue | 9 +- .../frontend/src/pages/registry.value.vue | 9 +- packages/frontend/src/pages/registry.vue | 9 +- .../frontend/src/pages/reset-password.vue | 12 +- packages/frontend/src/pages/role.vue | 12 +- packages/frontend/src/pages/scratchpad.vue | 22 +- packages/frontend/src/pages/search.event.vue | 8 +- packages/frontend/src/pages/search.note.vue | 238 +- .../frontend/src/pages/search.stories.impl.ts | 88 - packages/frontend/src/pages/search.user.vue | 84 +- packages/frontend/src/pages/search.vue | 65 +- .../src/pages/settings/2fa.qrdialog.vue | 29 +- packages/frontend/src/pages/settings/2fa.vue | 26 +- .../src/pages/settings/account-stats.vue | 9 +- .../frontend/src/pages/settings/accounts.vue | 19 +- packages/frontend/src/pages/settings/api.vue | 16 +- packages/frontend/src/pages/settings/apps.vue | 13 +- .../settings/avatar-decoration.decoration.vue | 8 +- .../settings/avatar-decoration.dialog.vue | 14 +- .../src/pages/settings/avatar-decoration.vue | 20 +- .../src/pages/settings/cherrypick.vue | 13 +- .../src/pages/settings/custom-css.vue | 6 +- packages/frontend/src/pages/settings/deck.vue | 6 +- .../src/pages/settings/drive-cleaner.vue | 15 +- .../frontend/src/pages/settings/drive.vue | 29 +- .../frontend/src/pages/settings/email.vue | 29 +- .../src/pages/settings/emoji-picker.vue | 14 +- .../frontend/src/pages/settings/general.vue | 89 +- .../src/pages/settings/import-export.vue | 43 +- .../frontend/src/pages/settings/index.vue | 18 +- .../frontend/src/pages/settings/migration.vue | 25 +- .../settings/mute-block.instance-mute.vue | 12 +- .../src/pages/settings/mute-block.vue | 19 +- .../pages/settings/mute-block.word-mute.vue | 4 +- .../frontend/src/pages/settings/navbar.vue | 6 +- .../notifications.notification-config.vue | 3 +- .../src/pages/settings/notifications.vue | 34 +- .../frontend/src/pages/settings/other.vue | 21 +- .../src/pages/settings/plugin.install.vue | 6 +- .../frontend/src/pages/settings/plugin.vue | 28 +- .../pages/settings/preferences-backups.vue | 16 +- .../frontend/src/pages/settings/privacy.vue | 29 +- .../frontend/src/pages/settings/profile.vue | 29 +- .../frontend/src/pages/settings/roles.vue | 15 +- .../frontend/src/pages/settings/security.vue | 9 +- .../pages/settings/sounds-and-vibrations.vue | 20 +- .../src/pages/settings/sounds.sound.vue | 51 +- .../pages/settings/statusbar.statusbar.vue | 6 +- .../frontend/src/pages/settings/statusbar.vue | 10 +- .../src/pages/settings/theme.install.vue | 16 +- .../src/pages/settings/theme.manage.vue | 8 +- .../frontend/src/pages/settings/theme.vue | 27 +- .../frontend/src/pages/settings/timeline.vue | 6 +- .../src/pages/settings/webhook.edit.vue | 15 +- .../src/pages/settings/webhook.new.vue | 8 +- .../frontend/src/pages/settings/webhook.vue | 6 +- packages/frontend/src/pages/share.vue | 50 +- .../frontend/src/pages/signup-complete.vue | 7 +- packages/frontend/src/pages/tag.vue | 9 +- packages/frontend/src/pages/theme-editor.vue | 10 +- packages/frontend/src/pages/timeline.vue | 280 +- .../frontend/src/pages/user-list-timeline.vue | 14 +- packages/frontend/src/pages/user-tag.vue | 6 +- .../frontend/src/pages/user/achievements.vue | 2 +- .../src/pages/user/activity.following.vue | 6 +- .../src/pages/user/activity.heatmap.vue | 219 + .../src/pages/user/activity.notes.vue | 6 +- .../frontend/src/pages/user/activity.pv.vue | 6 +- packages/frontend/src/pages/user/activity.vue | 6 +- packages/frontend/src/pages/user/clips.vue | 2 +- packages/frontend/src/pages/user/events.vue | 2 +- packages/frontend/src/pages/user/flashs.vue | 2 +- .../frontend/src/pages/user/follow-list.vue | 2 +- .../frontend/src/pages/user/followers.vue | 21 +- .../frontend/src/pages/user/following.vue | 21 +- packages/frontend/src/pages/user/gallery.vue | 2 +- .../src/pages/user/home.stories.impl.ts | 26 +- packages/frontend/src/pages/user/home.vue | 68 +- .../src/pages/user/index.activity.vue | 2 +- .../frontend/src/pages/user/index.files.vue | 6 +- .../src/pages/user/index.timeline.vue | 4 +- packages/frontend/src/pages/user/index.vue | 65 +- packages/frontend/src/pages/user/lists.vue | 2 +- packages/frontend/src/pages/user/pages.vue | 2 +- packages/frontend/src/pages/user/raw.vue | 2 +- .../frontend/src/pages/user/reactions.vue | 2 +- .../frontend/src/pages/welcome.entrance.a.vue | 39 +- packages/frontend/src/pages/welcome.setup.vue | 5 +- .../src/pages/welcome.timeline.note.vue | 109 - .../frontend/src/pages/welcome.timeline.vue | 104 +- packages/frontend/src/pages/welcome.vue | 18 +- packages/frontend/src/pizzax.ts | 41 +- packages/frontend/src/plugin.ts | 44 +- packages/frontend/src/router.ts | 602 + packages/frontend/src/router/definition.ts | 665 - packages/frontend/src/router/main.ts | 167 - packages/frontend/src/router/supplier.ts | 30 - packages/frontend/src/scripts/achievements.ts | 48 +- .../add-dividers-between-menu-sections.ts | 5 - packages/frontend/src/scripts/aiscript/api.ts | 17 +- packages/frontend/src/scripts/aiscript/ui.ts | 68 +- .../src/scripts/{misskey-api.ts => api.ts} | 28 +- packages/frontend/src/scripts/array.ts | 40 +- packages/frontend/src/scripts/autocomplete.ts | 47 +- packages/frontend/src/scripts/cache.ts | 2 +- packages/frontend/src/scripts/chart-legend.ts | 2 +- packages/frontend/src/scripts/chart-vline.ts | 2 +- .../frontend/src/scripts/check-permissions.ts | 19 - .../src/scripts/check-reaction-permissions.ts | 17 - .../frontend/src/scripts/check-word-mute.ts | 2 +- packages/frontend/src/scripts/clear-cache.ts | 9 - packages/frontend/src/scripts/clicker-game.ts | 8 +- packages/frontend/src/scripts/clone.ts | 8 +- .../frontend/src/scripts/code-highlighter.ts | 95 +- packages/frontend/src/scripts/collapsed.ts | 13 +- .../frontend/src/scripts/collect-page-vars.ts | 2 +- packages/frontend/src/scripts/color.ts | 2 +- packages/frontend/src/scripts/confetti.ts | 2 +- packages/frontend/src/scripts/contains.ts | 2 +- .../frontend/src/scripts/copy-to-clipboard.ts | 33 +- .../frontend/src/scripts/detect-language.ts | 5 - packages/frontend/src/scripts/device-kind.ts | 9 +- .../frontend/src/scripts/edit-nickname.ts | 2 +- packages/frontend/src/scripts/emoji-base.ts | 2 +- packages/frontend/src/scripts/emoji-picker.ts | 2 +- packages/frontend/src/scripts/emojilist.ts | 32 +- .../extract-avg-color-from-blurhash.ts | 2 +- .../frontend/src/scripts/extract-mentions.ts | 2 +- .../src/scripts/extract-url-from-mfm.ts | 2 +- packages/frontend/src/scripts/focus-trap.ts | 78 - packages/frontend/src/scripts/focus.ts | 98 +- packages/frontend/src/scripts/form.ts | 61 +- .../src/scripts/format-time-string.ts | 2 +- .../frontend/src/scripts/gen-search-query.ts | 4 +- .../src/scripts/get-account-from-id.ts | 2 +- .../src/scripts/get-dom-node-or-null.ts | 19 - .../src/scripts/get-drive-file-menu.ts | 33 +- .../frontend/src/scripts/get-note-menu.ts | 353 +- .../frontend/src/scripts/get-note-summary.ts | 10 +- .../frontend/src/scripts/get-user-menu.ts | 78 +- .../frontend/src/scripts/get-user-name.ts | 2 +- packages/frontend/src/scripts/hotkey.ts | 205 +- packages/frontend/src/scripts/i18n.ts | 294 +- packages/frontend/src/scripts/idb-proxy.ts | 12 +- packages/frontend/src/scripts/idle-render.ts | 2 +- packages/frontend/src/scripts/init-chart.ts | 2 +- .../frontend/src/scripts/initialize-sw.ts | 2 +- .../frontend/src/scripts/install-plugin.ts | 10 +- .../frontend/src/scripts/install-theme.ts | 2 +- packages/frontend/src/scripts/intl-const.ts | 6 +- .../src/scripts/is-device-darkmode.ts | 2 +- .../frontend/src/scripts/isFfVisibleForMe.ts | 2 +- packages/frontend/src/scripts/keycode.ts | 23 + packages/frontend/src/scripts/langmap.ts | 2 +- packages/frontend/src/scripts/login-id.ts | 2 +- .../{admin-lookup.ts => lookup-user.ts} | 30 +- packages/frontend/src/scripts/lookup.ts | 9 +- .../frontend/src/scripts/media-has-audio.ts | 5 - packages/frontend/src/scripts/media-proxy.ts | 2 +- packages/frontend/src/scripts/merge.ts | 35 - .../src/scripts/mfm-function-picker.ts | 35 +- packages/frontend/src/scripts/navigator.ts | 2 +- packages/frontend/src/scripts/nyaize.ts | 2 +- .../frontend/src/scripts/page-metadata.ts | 74 +- packages/frontend/src/scripts/physics.ts | 2 +- .../src/scripts/player-url-transform.ts | 26 - packages/frontend/src/scripts/please-login.ts | 48 +- packages/frontend/src/scripts/popout.ts | 2 +- .../frontend/src/scripts/popup-position.ts | 42 +- packages/frontend/src/scripts/post-message.ts | 2 +- .../frontend/src/scripts/reaction-picker.ts | 8 +- packages/frontend/src/scripts/safe-parse.ts | 11 - .../frontend/src/scripts/safe-uri-decode.ts | 2 +- packages/frontend/src/scripts/scroll.ts | 10 +- packages/frontend/src/scripts/search-emoji.ts | 106 - packages/frontend/src/scripts/select-file.ts | 5 +- .../frontend/src/scripts/show-moved-dialog.ts | 2 +- .../src/scripts/show-suspended-dialog.ts | 2 +- packages/frontend/src/scripts/shuffle.ts | 2 +- .../frontend/src/scripts/snowfall-effect.ts | 28 +- packages/frontend/src/scripts/sound.ts | 145 +- .../frontend/src/scripts/sticky-sidebar.ts | 2 +- packages/frontend/src/scripts/test-utils.ts | 2 +- packages/frontend/src/scripts/theme-editor.ts | 2 +- packages/frontend/src/scripts/theme.ts | 70 +- packages/frontend/src/scripts/time.ts | 2 +- packages/frontend/src/scripts/timezones.ts | 2 +- packages/frontend/src/scripts/touch.ts | 6 +- .../frontend/src/scripts/unison-reload.ts | 2 +- packages/frontend/src/scripts/upload.ts | 14 +- .../src/scripts/upload/compress-config.ts | 6 +- .../src/scripts/upload/isWebpSupported.ts | 2 +- packages/frontend/src/scripts/url.ts | 7 +- .../frontend/src/scripts/use-chart-tooltip.ts | 18 +- .../src/scripts/use-document-visibility.ts | 2 +- packages/frontend/src/scripts/use-interval.ts | 14 +- .../frontend/src/scripts/use-leave-guard.ts | 2 +- .../frontend/src/scripts/use-note-capture.ts | 10 +- packages/frontend/src/scripts/use-tooltip.ts | 2 +- .../src/scripts/worker-multi-dispatch.ts | 2 +- packages/frontend/src/store.ts | 80 +- packages/frontend/src/stream.ts | 24 +- packages/frontend/src/style.scss | 99 +- packages/frontend/src/theme-store.ts | 10 +- packages/frontend/src/themes/_dark.json5 | 4 - packages/frontend/src/themes/_light.json5 | 4 - .../src/themes/catppuccin-frappe-blue.json5 | 94 - .../themes/catppuccin-frappe-flamingo.json5 | 94 - .../src/themes/catppuccin-frappe-green.json5 | 94 - .../themes/catppuccin-frappe-lavender.json5 | 94 - .../src/themes/catppuccin-frappe-maroon.json5 | 94 - .../src/themes/catppuccin-frappe-mauve.json5 | 94 - .../src/themes/catppuccin-frappe-peach.json5 | 94 - .../src/themes/catppuccin-frappe-pink.json5 | 94 - .../src/themes/catppuccin-frappe-red.json5 | 94 - .../themes/catppuccin-frappe-rosewater.json5 | 94 - .../themes/catppuccin-frappe-sapphire.json5 | 94 - .../src/themes/catppuccin-frappe-sky.json5 | 94 - .../src/themes/catppuccin-frappe-teal.json5 | 94 - .../src/themes/catppuccin-frappe-yellow.json5 | 94 - .../src/themes/catppuccin-latte-blue.json5 | 94 - .../themes/catppuccin-latte-flamingo.json5 | 94 - .../src/themes/catppuccin-latte-green.json5 | 94 - .../themes/catppuccin-latte-lavender.json5 | 94 - .../src/themes/catppuccin-latte-maroon.json5 | 94 - .../src/themes/catppuccin-latte-mauve.json5 | 94 - .../src/themes/catppuccin-latte-peach.json5 | 94 - .../src/themes/catppuccin-latte-pink.json5 | 94 - .../src/themes/catppuccin-latte-red.json5 | 94 - .../themes/catppuccin-latte-rosewater.json5 | 94 - .../themes/catppuccin-latte-sapphire.json5 | 94 - .../src/themes/catppuccin-latte-sky.json5 | 94 - .../src/themes/catppuccin-latte-teal.json5 | 94 - .../src/themes/catppuccin-latte-yellow.json5 | 94 - .../themes/catppuccin-macchiato-blue.json5 | 94 - .../catppuccin-macchiato-flamingo.json5 | 94 - .../themes/catppuccin-macchiato-green.json5 | 94 - .../catppuccin-macchiato-lavender.json5 | 94 - .../themes/catppuccin-macchiato-maroon.json5 | 94 - .../themes/catppuccin-macchiato-mauve.json5 | 94 - .../themes/catppuccin-macchiato-peach.json5 | 94 - .../themes/catppuccin-macchiato-pink.json5 | 94 - .../src/themes/catppuccin-macchiato-red.json5 | 94 - .../catppuccin-macchiato-rosewater.json5 | 94 - .../catppuccin-macchiato-sapphire.json5 | 94 - .../src/themes/catppuccin-macchiato-sky.json5 | 94 - .../themes/catppuccin-macchiato-teal.json5 | 94 - .../themes/catppuccin-macchiato-yellow.json5 | 94 - .../src/themes/catppuccin-mocha-blue.json5 | 94 - .../themes/catppuccin-mocha-flamingo.json5 | 94 - .../src/themes/catppuccin-mocha-green.json5 | 94 - .../themes/catppuccin-mocha-lavender.json5 | 94 - .../src/themes/catppuccin-mocha-maroon.json5 | 94 - .../src/themes/catppuccin-mocha-mauve.json5 | 94 - .../src/themes/catppuccin-mocha-peach.json5 | 94 - .../src/themes/catppuccin-mocha-pink.json5 | 94 - .../src/themes/catppuccin-mocha-red.json5 | 94 - .../themes/catppuccin-mocha-rosewater.json5 | 94 - .../themes/catppuccin-mocha-sapphire.json5 | 94 - .../src/themes/catppuccin-mocha-sky.json5 | 94 - .../src/themes/catppuccin-mocha-teal.json5 | 94 - .../src/themes/catppuccin-mocha-yellow.json5 | 94 - packages/frontend/src/timelines.ts | 62 - packages/frontend/src/type.ts | 8 - .../frontend/src/types/date-separated-list.ts | 2 +- packages/frontend/src/types/menu.ts | 16 +- packages/frontend/src/types/page-header.ts | 2 +- .../src/ui/_common_/announcements.vue | 4 +- packages/frontend/src/ui/_common_/common.ts | 48 +- packages/frontend/src/ui/_common_/common.vue | 7 +- .../src/ui/_common_/navbar-for-mobile.vue | 24 +- packages/frontend/src/ui/_common_/navbar.vue | 106 +- .../frontend/src/ui/_common_/notification.vue | 2 +- .../src/ui/_common_/statusbar-federation.vue | 6 +- .../src/ui/_common_/statusbar-rss.vue | 7 +- .../src/ui/_common_/statusbar-user-list.vue | 6 +- .../frontend/src/ui/_common_/statusbars.vue | 2 +- .../src/ui/_common_/stream-indicator.vue | 2 +- .../frontend/src/ui/_common_/sw-inject.ts | 11 +- packages/frontend/src/ui/_common_/upload.vue | 2 +- packages/frontend/src/ui/classic.header.vue | 14 +- packages/frontend/src/ui/classic.sidebar.vue | 17 +- packages/frontend/src/ui/classic.vue | 24 +- packages/frontend/src/ui/deck.vue | 62 +- .../frontend/src/ui/deck/antenna-column.vue | 65 +- .../frontend/src/ui/deck/channel-column.vue | 46 +- packages/frontend/src/ui/deck/column.vue | 6 +- packages/frontend/src/ui/deck/deck-store.ts | 38 +- .../frontend/src/ui/deck/direct-column.vue | 4 +- packages/frontend/src/ui/deck/list-column.vue | 68 +- packages/frontend/src/ui/deck/main-column.vue | 20 +- .../frontend/src/ui/deck/mentions-column.vue | 4 +- .../src/ui/deck/notifications-column.vue | 24 +- .../src/ui/deck/role-timeline-column.vue | 32 +- packages/frontend/src/ui/deck/tl-column.vue | 54 +- .../src/ui/deck/tl-note-notification.ts | 107 - .../frontend/src/ui/deck/widgets-column.vue | 2 +- packages/frontend/src/ui/friendly.vue | 39 +- .../src/ui/friendly/navbar-for-mobile.vue | 22 +- packages/frontend/src/ui/friendly/navbar.vue | 140 +- packages/frontend/src/ui/minimum.vue | 24 +- packages/frontend/src/ui/universal.vue | 26 +- .../frontend/src/ui/universal.widgets.vue | 2 +- packages/frontend/src/ui/visitor.vue | 53 +- packages/frontend/src/ui/zen.vue | 24 +- .../src/unicode-emoji-indexes/ja-JP.json | 1866 - .../src/unicode-emoji-indexes/ja-JP_hira.json | 1866 - .../src/widgets/WidgetActivity.calendar.vue | 2 +- .../src/widgets/WidgetActivity.chart.vue | 2 +- .../frontend/src/widgets/WidgetActivity.vue | 6 +- .../frontend/src/widgets/WidgetAichan.vue | 2 +- .../frontend/src/widgets/WidgetAiscript.vue | 18 +- .../src/widgets/WidgetAiscriptApp.vue | 18 +- .../src/widgets/WidgetBirthdayFollowings.vue | 39 +- .../frontend/src/widgets/WidgetButton.vue | 18 +- .../frontend/src/widgets/WidgetCalendar.vue | 12 +- .../frontend/src/widgets/WidgetClicker.vue | 2 +- packages/frontend/src/widgets/WidgetClock.vue | 2 +- packages/frontend/src/widgets/WidgetDice.vue | 70 - .../src/widgets/WidgetDigitalClock.vue | 2 +- .../frontend/src/widgets/WidgetFederation.vue | 8 +- .../src/widgets/WidgetInstanceCloud.vue | 5 +- .../src/widgets/WidgetInstanceInfo.vue | 9 +- .../frontend/src/widgets/WidgetJobQueue.vue | 27 +- packages/frontend/src/widgets/WidgetMemo.vue | 2 +- .../src/widgets/WidgetNotifications.vue | 33 +- .../src/widgets/WidgetOnlineUsers.vue | 6 +- .../frontend/src/widgets/WidgetPhotos.vue | 6 +- .../frontend/src/widgets/WidgetPostForm.vue | 2 +- .../frontend/src/widgets/WidgetProfile.vue | 9 +- packages/frontend/src/widgets/WidgetRss.vue | 9 +- .../frontend/src/widgets/WidgetRssTicker.vue | 9 +- .../frontend/src/widgets/WidgetSearch.vue | 130 - .../frontend/src/widgets/WidgetSlideshow.vue | 11 +- .../frontend/src/widgets/WidgetTimeline.vue | 44 +- .../frontend/src/widgets/WidgetTrends.vue | 8 +- .../frontend/src/widgets/WidgetUnixClock.vue | 8 +- .../frontend/src/widgets/WidgetUserList.vue | 9 +- packages/frontend/src/widgets/index.ts | 6 +- .../widgets/server-metric/cpu-mem-net-pie.vue | 8 +- .../src/widgets/server-metric/cpu-mem-pie.vue | 14 +- .../src/widgets/server-metric/cpu-mem.vue | 13 +- .../src/widgets/server-metric/cpu.vue | 6 +- .../src/widgets/server-metric/disk.vue | 2 +- .../src/widgets/server-metric/index.vue | 15 +- .../src/widgets/server-metric/mem.vue | 6 +- .../src/widgets/server-metric/net-pie.vue | 14 +- .../src/widgets/server-metric/net.vue | 13 +- .../src/widgets/server-metric/pie-compact.vue | 2 +- .../src/widgets/server-metric/pie.vue | 2 +- packages/frontend/src/widgets/widget.ts | 2 +- .../frontend/src/workers/draw-blurhash.ts | 2 +- packages/frontend/src/workers/test-webgl2.ts | 2 +- packages/frontend/test/autocomplete.test.ts | 34 - packages/frontend/test/emoji.test.ts | 41 - packages/frontend/test/home.test.ts | 2 +- packages/frontend/test/init.ts | 26 +- packages/frontend/test/note.test.ts | 2 +- packages/frontend/test/scroll.test.ts | 8 +- packages/frontend/test/url-preview.test.ts | 164 + packages/frontend/tsconfig.json | 5 +- packages/frontend/vite.config.local-dev.ts | 61 +- packages/frontend/vite.config.ts | 52 +- packages/misskey-bubble-game/build.js | 104 - packages/misskey-bubble-game/eslint.config.js | 27 - packages/misskey-bubble-game/package.json | 44 - packages/misskey-bubble-game/src/game.ts | 495 - packages/misskey-bubble-game/src/index.ts | 14 - packages/misskey-bubble-game/src/monos.ts | 952 - packages/misskey-bubble-game/tsconfig.json | 33 - packages/shared/.eslintrc.js | 118 + packages/shared/eslint.config.js | 28 - packages/shared/package.json | 3 - packages/sw/.eslintrc.cjs | 21 + packages/sw/biome.json | 119 - packages/sw/build.js | 4 +- packages/sw/eslint.config.js | 33 - packages/sw/package.json | 14 +- packages/sw/src/@types/global.d.ts | 2 +- .../sw/src/scripts/create-notification.ts | 8 +- .../sw/src/scripts/get-account-from-id.ts | 2 +- packages/sw/src/scripts/get-user-name.ts | 2 +- packages/sw/src/scripts/i18n.ts | 2 +- packages/sw/src/scripts/lang.ts | 2 +- packages/sw/src/scripts/login-id.ts | 2 +- packages/sw/src/scripts/operations.ts | 2 +- packages/sw/src/scripts/twemoji-base.ts | 2 +- packages/sw/src/sw.ts | 21 +- packages/sw/src/types.ts | 2 +- packages/sw/tsconfig.json | 1 + pnpm-lock.yaml | 32625 +++++++--------- pnpm-workspace.yaml | 1 - scripts/build-assets.mjs | 31 +- scripts/build-pre.js | 2 +- scripts/changelog-checker/.gitignore | 3 - scripts/changelog-checker/eslint.config.js | 17 - scripts/changelog-checker/package-lock.json | 2823 -- scripts/changelog-checker/package.json | 24 - scripts/changelog-checker/src/checker.ts | 92 - scripts/changelog-checker/src/index.ts | 38 - scripts/changelog-checker/src/parser.ts | 67 - .../changelog-checker/test/checker.test.ts | 419 - scripts/changelog-checker/tsconfig.json | 31 - scripts/changelog-checker/vite.config.ts | 6 - scripts/clean-all.js | 11 +- scripts/clean.js | 4 +- scripts/dev.mjs | 44 +- scripts/tarball.mjs | 37 - 2438 files changed, 34498 insertions(+), 103964 deletions(-) rename .devcontainer/{compose.yml => docker-compose.yml} (93%) create mode 100644 .github/FUNDING.yml delete mode 100644 .github/ISSUE_TEMPLATE/03_dev.yml delete mode 100644 .github/workflows/changelog-check.yml delete mode 100644 .github/workflows/check-cherrypick-js-autogen.yml delete mode 100644 .github/workflows/check-cherrypick-js-version.yml delete mode 100644 .github/workflows/check-spdx-license-id.yml delete mode 100644 .github/workflows/deploy-test-environment.yml delete mode 100644 .github/workflows/locale.yml delete mode 100644 .github/workflows/on-release-created.yml delete mode 100644 .github/workflows/release-edit-with-push.yml delete mode 100644 .github/workflows/release-with-dispatch.yml delete mode 100644 .github/workflows/release-with-ready.yml delete mode 100644 .github/workflows/scorecard.yml delete mode 100644 .github/workflows/storybook.yml delete mode 100644 .github/workflows/validate-api-json.yml delete mode 100644 CHANGELOG_engawa.md delete mode 100644 CHANGELOG_yojo.md delete mode 100644 compose.local-db.yml delete mode 100644 compose_example.yml rename cypress/e2e/{basic.cy.ts => basic.cy.js} (93%) delete mode 100644 cypress/e2e/router.cy.ts rename cypress/e2e/{widgets.cy.ts => widgets.cy.js} (95%) rename cypress/support/{commands.ts => commands.js} (87%) rename cypress/support/{e2e.ts => e2e.js} (100%) delete mode 100644 cypress/support/index.ts delete mode 100644 cypress/tsconfig.json create mode 100644 docker-compose.local-db.yml create mode 100644 docker-compose_example.yml delete mode 100644 docs/Advanced-Search.md delete mode 100644 docs/DEVELOPMENT.md delete mode 100644 locales/verify.js create mode 160000 misskey-assets create mode 100644 packages/backend/.eslintignore create mode 100644 packages/backend/.eslintrc.cjs delete mode 100644 packages/backend/assets/api-doc.html create mode 100644 packages/backend/assets/redoc.html delete mode 100644 packages/backend/biome.json rename packages/backend/{scripts => }/check_connect.js (65%) delete mode 100644 packages/backend/eslint.config.js create mode 100644 packages/backend/generate_api_json.js delete mode 100644 packages/backend/jest.config.e2e.cjs delete mode 100644 packages/backend/jest.config.unit.cjs delete mode 100644 packages/backend/migration/1703658526000-supportTrueMailApi.js delete mode 100644 packages/backend/migration/1704373210054-support-mcaptcha.js delete mode 100644 packages/backend/migration/1704959805077-bubble-game-record.js delete mode 100644 packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js delete mode 100644 packages/backend/migration/1705475608437-reversi.js delete mode 100644 packages/backend/migration/1705654039457-reversi-2.js delete mode 100644 packages/backend/migration/1705793785675-reversi-3.js delete mode 100644 packages/backend/migration/1705794768153-reversi-4.js delete mode 100644 packages/backend/migration/1705798904141-reversi-5.js delete mode 100644 packages/backend/migration/1706081514499-reversi-6.js delete mode 100644 packages/backend/migration/1706791962000-fix-meta-disableRegistration.js delete mode 100644 packages/backend/migration/1707429690000-prohibited-words.js delete mode 100644 packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js delete mode 100644 packages/backend/migration/1708266695091-repositoryUrl-from-syuilo-to-misskey-dev.js delete mode 100644 packages/backend/migration/1708399372194-per-instance-mod-note.js delete mode 100644 packages/backend/migration/1710210658917-AddSomeUrl.js delete mode 100644 packages/backend/migration/1710512074000-url-preview-meta.js delete mode 100644 packages/backend/migration/1710919614510-antenna-exclude-bots.js delete mode 100644 packages/backend/migration/1711722198590-no-recursive-delete.js delete mode 100644 packages/backend/migration/1713656541000-abuse-report-notification.js delete mode 100644 packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js delete mode 100644 packages/backend/migration/1716197366117-MediaSilenceForHosts.js delete mode 100644 packages/backend/migration/1716345015347-NotRespondingSince.js delete mode 100644 packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js delete mode 100644 packages/backend/migration/1716450883149-RemoveAntennaNotify.js delete mode 100644 packages/backend/migration/1717117195275-inquiryUrl.js delete mode 100644 packages/backend/migration/1717644139656-addDirectSummalyProxy.js delete mode 100644 packages/backend/migration/1720161864577-AddDeleteAt.js delete mode 100644 packages/backend/migration/1721299883211-AddIsIndexable.js delete mode 100644 packages/backend/migration/1721550461923-AddPrivateVisibility.js delete mode 100644 packages/backend/migration/1721666053703-fixDriveUrl.js delete mode 100644 packages/backend/migration/1722350613009-AddIsSensitive.js delete mode 100644 packages/backend/migration/1722353293595-AddSensitiveProfile.js delete mode 100644 packages/backend/scripts/dev.mjs delete mode 100644 packages/backend/scripts/generate_api_json.js delete mode 100644 packages/backend/src/boot/ready.ts delete mode 100644 packages/backend/src/core/AbuseReportNotificationService.ts delete mode 100644 packages/backend/src/core/AbuseReportService.ts delete mode 100644 packages/backend/src/core/SystemWebhookService.ts delete mode 100644 packages/backend/src/core/UserRenoteMutingService.ts delete mode 100644 packages/backend/src/core/UserSearchService.ts delete mode 100644 packages/backend/src/core/UserWebhookService.ts create mode 100644 packages/backend/src/core/WebhookService.ts rename packages/backend/src/core/activitypub/{JsonLdService.ts => LdSignatureService.ts} (79%) delete mode 100644 packages/backend/src/core/activitypub/misc/validator.ts delete mode 100644 packages/backend/src/core/deserializeAntenna.ts delete mode 100644 packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts delete mode 100644 packages/backend/src/core/entities/AnnouncementEntityService.ts delete mode 100644 packages/backend/src/core/entities/MetaEntityService.ts delete mode 100644 packages/backend/src/core/entities/SystemWebhookEntityService.ts delete mode 100644 packages/backend/src/misc/FileWriterStream.ts delete mode 100644 packages/backend/src/misc/JsonArrayStream.ts delete mode 100644 packages/backend/src/misc/fastify-hook-handlers.ts create mode 100644 packages/backend/src/misc/is-not-null.ts create mode 100644 packages/backend/src/misc/is-pure-renote.ts create mode 100644 packages/backend/src/misc/is-quote.ts delete mode 100644 packages/backend/src/misc/is-renote.ts delete mode 100644 packages/backend/src/misc/json-value.ts delete mode 100644 packages/backend/src/misc/password.ts create mode 100644 packages/backend/src/misc/prelude/math.ts create mode 100644 packages/backend/src/misc/prelude/maybe.ts create mode 100644 packages/backend/src/misc/prelude/string.ts create mode 100644 packages/backend/src/misc/prelude/symbol.ts delete mode 100644 packages/backend/src/misc/promise-tracker.ts delete mode 100644 packages/backend/src/models/AbuseReportNotificationRecipient.ts delete mode 100644 packages/backend/src/models/BubbleGameRecord.ts delete mode 100644 packages/backend/src/models/SystemWebhook.ts delete mode 100644 packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts delete mode 100644 packages/backend/src/models/json-schema/meta.ts delete mode 100644 packages/backend/src/models/json-schema/system-webhook.ts delete mode 100644 packages/backend/src/queue/processors/ExportClipsProcessorService.ts delete mode 100644 packages/backend/src/queue/processors/ScheduledNoteDeleteProcessorService.ts delete mode 100644 packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts rename packages/backend/src/queue/processors/{UserWebhookDeliverProcessorService.ts => WebhookDeliverProcessorService.ts} (88%) delete mode 100644 packages/backend/src/server/HealthServerService.ts delete mode 100644 packages/backend/src/server/api/FtsQueryService.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/set-user-sensitive.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/unset-user-sensitive.ts delete mode 100644 packages/backend/src/server/api/endpoints/announcements/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/bubble-game/ranking.ts delete mode 100644 packages/backend/src/server/api/endpoints/bubble-game/register.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/export-clips.ts delete mode 100644 packages/backend/src/server/api/endpoints/notifications/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/notifications/flush.ts delete mode 100644 packages/backend/test-server/.swcrc delete mode 100644 packages/backend/test-server/entry.ts delete mode 100644 packages/backend/test-server/eslint.config.js delete mode 100644 packages/backend/test-server/tsconfig.json create mode 100644 packages/backend/test/.eslintrc.cjs rename packages/backend/test/{compose.yml => docker-compose.yml} (94%) delete mode 100644 packages/backend/test/e2e/drive.ts delete mode 100644 packages/backend/test/e2e/exports.ts delete mode 100644 packages/backend/test/e2e/fetch-validate-ap-deny.ts delete mode 100644 packages/backend/test/e2e/synalio/abuse-report.ts delete mode 100644 packages/backend/test/e2e/synalio/user-create.ts delete mode 100644 packages/backend/test/eslint.config.js delete mode 100644 packages/backend/test/global.d.ts delete mode 100644 packages/backend/test/jest.setup.ts create mode 100644 packages/backend/test/prelude/maybe.ts delete mode 100644 packages/backend/test/resources/192.jpg delete mode 100644 packages/backend/test/resources/192.png create mode 100644 packages/backend/test/resources/Lenna.jpg create mode 100644 packages/backend/test/resources/Lenna.png delete mode 100644 packages/backend/test/resources/kick_gaba7.m4a delete mode 100644 packages/backend/test/unit/AbuseReportNotificationService.ts delete mode 100644 packages/backend/test/unit/ApMfmService.ts delete mode 100644 packages/backend/test/unit/NoteCreateService.ts delete mode 100644 packages/backend/test/unit/SystemWebhookService.ts delete mode 100644 packages/backend/test/unit/UserSearchService.ts delete mode 100644 packages/backend/test/unit/entities/UserEntityService.ts delete mode 100644 packages/backend/test/unit/misc/is-renote.ts rename packages/backend/{scripts => }/watch.mjs (87%) create mode 100644 packages/cherrypick-js/.eslintignore create mode 100644 packages/cherrypick-js/.eslintrc.cjs delete mode 100644 packages/cherrypick-js/biome.json delete mode 100644 packages/cherrypick-js/build.js delete mode 100644 packages/cherrypick-js/eslint.config.js create mode 100644 packages/cherrypick-js/generator/.eslintrc.cjs delete mode 100644 packages/cherrypick-js/generator/eslint.config.js create mode 100644 packages/frontend/.eslintrc.cjs delete mode 100644 packages/frontend/.storybook/charts.ts delete mode 100644 packages/frontend/assets/drop-and-fusion/bgm_1.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/click.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/collision.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/collision_yen.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/drop-arrow.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/drop.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/drop_yen.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/dropper.png delete mode 100644 packages/frontend/assets/drop-and-fusion/frame-dark.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/frame-light.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/fusion.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/fusion_yen.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/gameover.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/gameover.png delete mode 100644 packages/frontend/assets/drop-and-fusion/gameover_yen.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/go.png delete mode 100644 packages/frontend/assets/drop-and-fusion/hold.mp3 delete mode 100644 packages/frontend/assets/drop-and-fusion/logo.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/cold_face.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/exploding_head.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/face_with_open_mouth.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/grinning_squinting_face.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/heart_suit.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/pleading_face.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_hearts.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_sunglasses.png delete mode 100644 packages/frontend/assets/drop-and-fusion/normal_monos/zany_face.png delete mode 100644 packages/frontend/assets/drop-and-fusion/ready.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_1.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_10.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_2.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_3.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_4.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_5.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_6.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_7.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_8.png delete mode 100644 packages/frontend/assets/drop-and-fusion/square_monos/keycap_9.png delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/candy_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/cookie_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/custard_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/doughnut_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/lollipop_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/pancakes_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/shortcake_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/candy_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/chocolate_bar_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/custard_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/lollipop_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/pancakes_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shaved_ice_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shortcake_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/sweets_monos/verts/soft_ice_cream_color.svg delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/10000yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/1000yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/100yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/10yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/1yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/2000yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/5000yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/500yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/50yen.png delete mode 100644 packages/frontend/assets/drop-and-fusion/yen_monos/5yen.png delete mode 100644 packages/frontend/assets/reversi/logo.png delete mode 100644 packages/frontend/assets/reversi/lose.mp3 delete mode 100644 packages/frontend/assets/reversi/matched.mp3 delete mode 100644 packages/frontend/assets/reversi/put.mp3 delete mode 100644 packages/frontend/assets/reversi/stone_b.png delete mode 100644 packages/frontend/assets/reversi/stone_w.png delete mode 100644 packages/frontend/assets/reversi/win.mp3 delete mode 100644 packages/frontend/assets/sounds/mujin/bubble.mp3 delete mode 100644 packages/frontend/assets/sounds/mujin/notification_1.mp3 delete mode 100644 packages/frontend/assets/sounds/mujin/test_notification.mp3 delete mode 100644 packages/frontend/biome.json delete mode 100644 packages/frontend/eslint.config.js delete mode 100644 packages/frontend/src/components/MkAntennaEditor.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkAntennaEditorDialog.vue delete mode 100644 packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkChannelList.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkChannelPreview.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkChart.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkChartLegend.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkChartTooltip.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkClickerGame.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkClipPreview.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkCode.core.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkCode.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkCodeEditor.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkCodeInline.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkCodeInline.vue delete mode 100644 packages/frontend/src/components/MkColorInput.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkContainer.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkContextMenu.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkCropperDialog.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue delete mode 100644 packages/frontend/src/components/MkCwButton.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDeprecatedWarning.vue delete mode 100644 packages/frontend/src/components/MkDialog.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDice.vue delete mode 100644 packages/frontend/src/components/MkDivider.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDivider.vue delete mode 100644 packages/frontend/src/components/MkDonation.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDrive.file.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDrive.folder.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDrive.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDriveFileThumbnail.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkDriveWindow.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkEmojiPicker.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkEmojiPickerDialog.stories.impl.ts create mode 100644 packages/frontend/src/components/MkEmojiPickerWindow.vue delete mode 100644 packages/frontend/src/components/MkFormDialog.file.vue delete mode 100644 packages/frontend/src/components/MkHorizontalSwipe.vue delete mode 100644 packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts delete mode 100644 packages/frontend/src/components/MkMediaAudio.vue delete mode 100644 packages/frontend/src/components/MkMediaRange.vue delete mode 100644 packages/frontend/src/components/MkPreview.vue delete mode 100644 packages/frontend/src/components/MkQrcode.vue delete mode 100644 packages/frontend/src/components/MkScheduledNoteDelete.vue delete mode 100644 packages/frontend/src/components/MkSearchInput.vue delete mode 100644 packages/frontend/src/components/MkSearchResultWindow.vue delete mode 100644 packages/frontend/src/components/MkSourceCodeAvailablePopup.vue delete mode 100644 packages/frontend/src/components/MkSystemWebhookEditor.impl.ts delete mode 100644 packages/frontend/src/components/MkSystemWebhookEditor.vue delete mode 100644 packages/frontend/src/components/MkUserSensitiveCaution.vue delete mode 100644 packages/frontend/src/components/global/I18n.vue delete mode 100644 packages/frontend/src/components/global/NotificationPageHeader.vue create mode 100644 packages/frontend/src/components/global/i18n.ts create mode 100644 packages/frontend/src/components/page/block.type.ts delete mode 100644 packages/frontend/src/components/page/page.dynamic.vue delete mode 100644 packages/frontend/src/filters/hms.ts delete mode 100644 packages/frontend/src/filters/kmg.ts delete mode 100644 packages/frontend/src/pages/about.overview.vue delete mode 100644 packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue delete mode 100644 packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue delete mode 100644 packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue delete mode 100644 packages/frontend/src/pages/admin/system-webhook.item.vue delete mode 100644 packages/frontend/src/pages/admin/system-webhook.vue delete mode 100644 packages/frontend/src/pages/announcement.vue delete mode 100644 packages/frontend/src/pages/contact.vue delete mode 100644 packages/frontend/src/pages/drop-and-fusion.game.vue delete mode 100644 packages/frontend/src/pages/drop-and-fusion.vue create mode 100644 packages/frontend/src/pages/follow.vue delete mode 100644 packages/frontend/src/pages/games.vue rename packages/frontend/src/pages/{install-extensions.vue => install-extentions.vue} (96%) delete mode 100644 packages/frontend/src/pages/lookup.vue rename packages/frontend/src/{components/MkAntennaEditor.vue => pages/my-antennas/editor.vue} (61%) delete mode 100644 packages/frontend/src/pages/notifications-friendly.vue delete mode 100644 packages/frontend/src/pages/preview.vue delete mode 100644 packages/frontend/src/pages/search.stories.impl.ts create mode 100644 packages/frontend/src/pages/user/activity.heatmap.vue delete mode 100644 packages/frontend/src/pages/welcome.timeline.note.vue create mode 100644 packages/frontend/src/router.ts delete mode 100644 packages/frontend/src/router/definition.ts delete mode 100644 packages/frontend/src/router/main.ts delete mode 100644 packages/frontend/src/router/supplier.ts rename packages/frontend/src/scripts/{misskey-api.ts => api.ts} (68%) delete mode 100644 packages/frontend/src/scripts/check-permissions.ts delete mode 100644 packages/frontend/src/scripts/check-reaction-permissions.ts delete mode 100644 packages/frontend/src/scripts/focus-trap.ts delete mode 100644 packages/frontend/src/scripts/get-dom-node-or-null.ts create mode 100644 packages/frontend/src/scripts/keycode.ts rename packages/frontend/src/scripts/{admin-lookup.ts => lookup-user.ts} (60%) delete mode 100644 packages/frontend/src/scripts/merge.ts delete mode 100644 packages/frontend/src/scripts/player-url-transform.ts delete mode 100644 packages/frontend/src/scripts/safe-parse.ts delete mode 100644 packages/frontend/src/scripts/search-emoji.ts delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-blue.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-flamingo.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-green.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-lavender.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-maroon.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-mauve.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-peach.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-pink.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-red.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-rosewater.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-sapphire.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-sky.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-teal.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-frappe-yellow.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-blue.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-flamingo.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-green.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-lavender.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-maroon.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-mauve.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-peach.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-pink.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-red.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-rosewater.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-sapphire.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-sky.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-teal.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-latte-yellow.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-blue.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-flamingo.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-green.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-lavender.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-maroon.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-mauve.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-peach.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-pink.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-red.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-rosewater.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-sapphire.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-sky.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-teal.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-macchiato-yellow.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-blue.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-flamingo.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-green.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-lavender.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-maroon.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-mauve.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-peach.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-pink.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-red.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-rosewater.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-sapphire.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-sky.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-teal.json5 delete mode 100644 packages/frontend/src/themes/catppuccin-mocha-yellow.json5 delete mode 100644 packages/frontend/src/timelines.ts delete mode 100644 packages/frontend/src/type.ts delete mode 100644 packages/frontend/src/ui/deck/tl-note-notification.ts delete mode 100644 packages/frontend/src/unicode-emoji-indexes/ja-JP.json delete mode 100644 packages/frontend/src/unicode-emoji-indexes/ja-JP_hira.json delete mode 100644 packages/frontend/src/widgets/WidgetDice.vue delete mode 100644 packages/frontend/src/widgets/WidgetSearch.vue delete mode 100644 packages/frontend/test/autocomplete.test.ts delete mode 100644 packages/frontend/test/emoji.test.ts create mode 100644 packages/frontend/test/url-preview.test.ts delete mode 100644 packages/misskey-bubble-game/build.js delete mode 100644 packages/misskey-bubble-game/eslint.config.js delete mode 100644 packages/misskey-bubble-game/package.json delete mode 100644 packages/misskey-bubble-game/src/game.ts delete mode 100644 packages/misskey-bubble-game/src/index.ts delete mode 100644 packages/misskey-bubble-game/src/monos.ts delete mode 100644 packages/misskey-bubble-game/tsconfig.json create mode 100644 packages/shared/.eslintrc.js delete mode 100644 packages/shared/eslint.config.js delete mode 100644 packages/shared/package.json create mode 100644 packages/sw/.eslintrc.cjs delete mode 100644 packages/sw/biome.json delete mode 100644 packages/sw/eslint.config.js delete mode 100644 scripts/changelog-checker/.gitignore delete mode 100644 scripts/changelog-checker/eslint.config.js delete mode 100644 scripts/changelog-checker/package-lock.json delete mode 100644 scripts/changelog-checker/package.json delete mode 100644 scripts/changelog-checker/src/checker.ts delete mode 100644 scripts/changelog-checker/src/index.ts delete mode 100644 scripts/changelog-checker/src/parser.ts delete mode 100644 scripts/changelog-checker/test/checker.test.ts delete mode 100644 scripts/changelog-checker/tsconfig.json delete mode 100644 scripts/changelog-checker/vite.config.ts delete mode 100644 scripts/tarball.mjs diff --git a/.config/docker_example.env b/.config/docker_example.env index a57f08475c..2797c2f807 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -1,11 +1,4 @@ -# cherrypick settings -# CHERRYPICK_URL=https://example.tld/ - # db settings POSTGRES_PASSWORD=example-cherrypick-pass -# DATABASE_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_USER=example-cherrypick-user -# DATABASE_USER=${POSTGRES_USER} POSTGRES_DB=cherrypick -# DATABASE_DB=${POSTGRES_DB} -DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" diff --git a/.config/docker_example.yml b/.config/docker_example.yml index ba8b07538c..d492bee81d 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -6,7 +6,6 @@ #───┘ URL └───────────────────────────────────────────────────── # Final accessible URL seen by a user. -# You can set url from an environment variable instead. url: https://example.tld/ # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE @@ -39,11 +38,9 @@ db: port: 5432 # Database name - # You can set db from an environment variable instead. db: cherrypick # Auth - # You can set user and pass from environment variables instead. user: example-cherrypick-user pass: example-cherrypick-pass @@ -120,19 +117,6 @@ redis: # index: '' # scope: local -# ┌───────────────────────────┐ -#───┘ OpenSearch configuration └───────────────────────────── - -# You can use OpenSearch when you enabled AdvancedSearch. - -#opensearch: -# host: opensearch -# port: 9200 -# user: 'admin' -# pass: 'opensearch-adminpassword' #強めのパスワードじゃないと怒られる -# ssl: false -# index: 'instancename' #なんでもいい - # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── @@ -152,21 +136,6 @@ redis: id: 'aidx' -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -218,10 +187,6 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files endpoint -# remoteProxy: https://example.com/files/ -# remoteProxy: /files/ - # Proxy remote files (default: true) proxyRemoteFiles: true diff --git a/.config/example.yml b/.config/example.yml index 5c3b80dd9f..5ad6ae0fef 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -2,63 +2,6 @@ # CherryPick configuration #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# ┌──────────────────────────────┐ -#───┘ a boring but important thing └──────────────────────────── - -# -# First of all, let me tell you a story that may possibly be -# boring to you and possibly important to you. -# -# CherryPick is licensed under the AGPLv3 license. This license is -# known to be often misunderstood. Please read the following -# instructions carefully and select the appropriate option so -# that you do not negligently cause a license violation. -# - -# -------- -# Option 1: If you host CherryPick AS-IS (without any changes to -# the source code. forks are not included). -# -# Step 1: Congratulations! You don't need to do anything. - -# -------- -# Option 2: If you have made changes to the source code (forks -# are included) and publish a Git repository of source -# code. There should be no access restrictions on -# this repository. Strictly speaking, it doesn't have -# to be a Git repository, but you'll probably use Git! -# -# Step 1: Build and run the CherryPick server first. -# Step 2: Open in -# your browser with the administrator account. -# Step 3: Enter the URL of your Git repository in the -# "Repository URL" field. - -# -------- -# Option 3: If neither of the above applies to you. -# (In this case, the source code should be published -# on the CherryPick interface. IT IS NOT ENOUGH TO -# DISCLOSE THE SOURCE CODE WEHN 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 -# from the root directory of your codebase. So it is -# also recommended to check directory -# once after building and before activating the server -# to avoid ACCIDENTAL LEAKING OF SENSITIVE INFORMATION. -# To prevent certain files from being included in the -# tarball, add a glob pattern after line 15 in -# . DO NOT FORGET TO BUILD AFTER -# ENABLING THIS OPTION!) -# -# Step 1: Uncomment the following line. -# -# publishTarballInsteadOfProvideRepositoryUrl: true - # ┌─────┐ #───┘ URL └───────────────────────────────────────────────────── @@ -129,9 +72,6 @@ dbReplications: false # user: # pass: -# You can use PGroonga when enabled. -#pgroonga: false - # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -178,7 +118,7 @@ redis: # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── -# You can set scope to local (default value) or global +# You can set scope to local (default value) or global # (include notes from remote). #meilisearch: @@ -189,20 +129,6 @@ redis: # index: '' # scope: local - -# ┌───────────────────────────┐ -#───┘ OpenSearch configuration └───────────────────────────── - -# You can use OpenSearch when you enabled AdvancedSearch. - -#opensearch: -# host: localhost -# port: 9200 -# user: 'admin' -# pass: 'opensearch-adminpassword' #強めのパスワードじゃないと怒られる -# ssl: false -# index: 'instancename' #なんでもいい - # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── @@ -222,21 +148,6 @@ redis: id: 'aidx' -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -249,14 +160,14 @@ id: 'aidx' # Job concurrency per worker #deliverJobConcurrency: 128 #inboxJobConcurrency: 16 -#relationshipJobConcurrency: 16 -# What's relationshipJob?: +#relashionshipJobConcurrency: 16 +# What's relashionshipJob?: # Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations. # Job rate limiter #deliverJobPerSec: 128 #inboxJobPerSec: 32 -#relationshipJobPerSec: 64 +#relashionshipJobPerSec: 64 # Job attempts #deliverJobMaxAttempts: 12 @@ -298,10 +209,6 @@ proxyBypassHosts: # * Perform image compression (on a different server resource than the main process) #mediaProxy: https://example.com/proxy -# Proxy remote files endpoint -# remoteProxy: https://example.com/files/ -# remoteProxy: /files/ - # Proxy remote files (default: true) # Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. proxyRemoteFiles: true @@ -316,7 +223,7 @@ proxyRemoteFiles: true signToActivityPubGet: true # For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". +# but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a053090287..6ea82e6d0c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,27 +1,28 @@ { "name": "CherryPick", - "dockerComposeFile": "compose.yml", + "dockerComposeFile": "docker-compose.yml", "service": "app", "workspaceFolder": "/workspace", "features": { - "ghcr.io/devcontainers/features/node:1": { - "version": "20.16.0" + "ghcr.io/devcontainers-contrib/features/pnpm:2": { + "version": "8.9.2" }, - "ghcr.io/devcontainers-contrib/features/corepack:1": {} + "ghcr.io/devcontainers/features/node:1": { + "version": "20.10.0" + } }, "forwardPorts": [3000], - "postCreateCommand": "/bin/bash .devcontainer/init.sh", + "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh", "customizations": { "vscode": { "extensions": [ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", + "Vue.vscode-typescript-vue-plugin", "Orta.vscode-jest", "dbaeumer.vscode-eslint", - "mrmlnc.vscode-json5", - "ms-vscode.vscode-typescript-next", - "oderwat.indent-rainbow" + "mrmlnc.vscode-json5" ] } } diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 3c2128bae3..a16537fb9f 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -132,21 +132,6 @@ redis: id: 'aidx' -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── diff --git a/.devcontainer/compose.yml b/.devcontainer/docker-compose.yml similarity index 93% rename from .devcontainer/compose.yml rename to .devcontainer/docker-compose.yml index 8ef5722f2b..1bee63986e 100644 --- a/.devcontainer/compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,3 +1,5 @@ +version: '3.8' + services: app: build: @@ -6,7 +8,6 @@ services: volumes: - ../:/workspace:cached - - node_modules:/workspace/node_modules command: sleep infinity @@ -45,7 +46,6 @@ services: volumes: postgres-data: redis-data: - node_modules: networks: internal_network: diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index 55fb1e6fa6..bcad3e6d85 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -2,11 +2,8 @@ set -xe -sudo chown node node_modules -git config --global --add safe.directory /workspace +sudo chown -R node /workspace git submodule update --init -corepack install -corepack enable pnpm config set store-dir /home/node/.local/share/pnpm/store pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml diff --git a/.dockerignore b/.dockerignore index 829df24f13..6b9ff75b2c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,11 +7,12 @@ Dockerfile build/ built/ db/ -.devcontainer/compose.yml +docker-compose.yml node_modules/ packages/*/node_modules redis/ files/ +misskey-assets/ fluent-emojis/ .pnp.* @@ -27,6 +28,6 @@ fluent-emojis/ .idea/ packages/*/.vscode/ -packages/backend/test/compose.yml +packages/backend/test/docker-compose.yml .pnpm-store/ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..4b76e4a829 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +patreon: noridev diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml index 31d8cac56a..96911bca86 100644 --- a/.github/ISSUE_TEMPLATE/01_bug-report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug-report.yml @@ -1,6 +1,6 @@ name: 🐛 Bug Report description: Create a report to help us improve -labels: ["bug"] +labels: ["⚠️bug?"] body: - type: markdown @@ -53,8 +53,8 @@ body: Examples: * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4 * Browser: Chrome 113.0.5672.126 - * Server URL: cherrypick.example.com - * CherryPick: 4.x.x (Misskey: 2024.x.x) + * Server URL: kokonect.link + * CherryPick: 4.x.x (Misskey: 2023.x.x) value: | * Model and OS of the device(s): * Browser: @@ -74,11 +74,11 @@ body: Examples: * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "CherryPick install shell script", development environment - * CherryPick: 4.x.x (Misskey: 2024.x.x) + * CherryPick: 4.x.x (Misskey: 2023.x.x) * Node: 20.x.x * PostgreSQL: 15.x.x * Redis: 7.x.x - * OS and Architecture: Ubuntu 24.04.2 LTS aarch64 + * OS and Architecture: Ubuntu 22.04.2 LTS aarch64 value: | * Installation Method or Hosting Service: * CherryPick: diff --git a/.github/ISSUE_TEMPLATE/02_feature-request.yml b/.github/ISSUE_TEMPLATE/02_feature-request.yml index b2b982015c..8d7b0b2539 100644 --- a/.github/ISSUE_TEMPLATE/02_feature-request.yml +++ b/.github/ISSUE_TEMPLATE/02_feature-request.yml @@ -1,6 +1,6 @@ name: ✨ Feature Request description: Suggest an idea for this project -labels: ["Feature"] +labels: ["✨Feature"] body: - type: textarea diff --git a/.github/ISSUE_TEMPLATE/03_dev.yml b/.github/ISSUE_TEMPLATE/03_dev.yml deleted file mode 100644 index 4879473bb0..0000000000 --- a/.github/ISSUE_TEMPLATE/03_dev.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: About Development -description: 開発に関する問題 -labels: ["Dev"] - -body: - - type: markdown - attributes: - value: | - 開発に関する問題を報告してください。 - 重複するIssueを避けるために、問題がすでに報告されていないかを検索してください。 - - - type: textarea - attributes: - label: Summary - description: Tell us what you want - validations: - required: true - - - type: textarea - attributes: - label: Purpose - description: Why do you want this? - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d3f227839d..9ffc7a9ec1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,13 +2,6 @@ contact_links: - name: 💬 Misskey official Discord url: https://discord.gg/Wp8gVStHW3 about: Chat freely about Misskey - # 仮 - - name: 💬 Start discussion (Misskey) - url: https://github.com/misskey-dev/misskey/discussions - about: The official forum to join conversation and ask question - name: 💬 CherryPick official Discord url: https://discord.gg/V8qghB28Aj about: Chat freely about CherryPick - - name: 💬 Start discussion (CherryPick) - url: https://github.com/kokonect-link/cherrypick/discussions - about: The official forum to join conversation and ask question diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d4678ec5e0..5955f6b5d9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,32 +17,16 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 10 - # List dependencies required to be updated together, sharing the same version numbers. - # Those who simply have the common owner (e.g. @fastify) don't need to be listed. + # PNPM has an issue with dependabot. See: + # https://github.com/dependabot/dependabot-core/issues/7258 + # https://github.com/pnpm/pnpm/issues/6530 + # TODO: Restore this when the issue is solved + open-pull-requests-limit: 0 groups: - aws-sdk: + swc: patterns: - - "@aws-sdk/*" - bull-board: - patterns: - - "@bull-board/*" - nestjs: - patterns: - - "@nestjs/*" - slacc: - patterns: - - "slacc-*" + - "@swc/*" storybook: patterns: - "storybook*" - "@storybook/*" - swc-core: - patterns: - - "@swc/core*" - typescript-eslint: - patterns: - - "@typescript-eslint/*" - tensorflow: - patterns: - - "@tensorflow/*" diff --git a/.github/workflows/api-cherrypick-js.yml b/.github/workflows/api-cherrypick-js.yml index 6a2fb9f1c1..39bd0ad311 100644 --- a/.github/workflows/api-cherrypick-js.yml +++ b/.github/workflows/api-cherrypick-js.yml @@ -1,14 +1,7 @@ name: API report (cherrypick.js) -on: - push: - paths: - - packages/cherrypick-js/** - - .github/workflows/api-cherrypick-js.yml - pull_request: - paths: - - packages/cherrypick-js/** - - .github/workflows/api-cherrypick-js.yml +on: [push, pull_request] + jobs: report: @@ -21,7 +14,7 @@ jobs: - run: corepack enable - name: Setup Node.js - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.1 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml deleted file mode 100644 index d4e99f966e..0000000000 --- a/.github/workflows/changelog-check.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Check the description in CHANGELOG.md - -on: - pull_request: - branches: - - master - - develop - -jobs: - check-changelog: - runs-on: ubuntu-latest - - steps: - - name: Checkout head - uses: actions/checkout@v4.1.1 - - name: Setup Node.js - uses: actions/setup-node@v4.0.3 - with: - node-version-file: '.node-version' - - - name: Checkout base - run: | - mkdir _base - cp -r .git _base/.git - cd _base - git fetch --depth 1 origin ${{ github.base_ref }} - git checkout origin/${{ github.base_ref }} CHANGELOG.md - - - name: Copy to Checker directory for CHANGELOG-base.md - run: cp _base/CHANGELOG.md scripts/changelog-checker/CHANGELOG-base.md - - name: Copy to Checker directory for CHANGELOG-head.md - run: cp CHANGELOG.md scripts/changelog-checker/CHANGELOG-head.md - - name: diff - continue-on-error: true - run: diff -u CHANGELOG-base.md CHANGELOG-head.md - working-directory: scripts/changelog-checker - - - name: Setup Checker - run: npm install - working-directory: scripts/changelog-checker - - name: Run Checker - run: npm run run - working-directory: scripts/changelog-checker diff --git a/.github/workflows/check-cherrypick-js-autogen.yml b/.github/workflows/check-cherrypick-js-autogen.yml deleted file mode 100644 index 0cdb201b36..0000000000 --- a/.github/workflows/check-cherrypick-js-autogen.yml +++ /dev/null @@ -1,133 +0,0 @@ -name: Check CherryPick JS autogen - -on: - pull_request_target: - branches: - - master - - develop - - improve-cherrypick-js-autogen-check - paths: - - packages/backend/** - -jobs: - # pull_request_target safety: permissions: read-all, and there are no secrets used in this job - generate-cherrypick-js: - runs-on: ubuntu-latest - permissions: - contents: read - if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} - steps: - - name: checkout - uses: actions/checkout@v4.1.1 - with: - submodules: true - ref: refs/pull/${{ github.event.pull_request.number }}/merge - - - name: setup pnpm - uses: pnpm/action-setup@v4 - - - name: setup node - id: setup-node - uses: actions/setup-node@v4.0.3 - with: - node-version-file: '.node-version' - cache: pnpm - - - name: install dependencies - run: pnpm i --frozen-lockfile - - # generate api.json - - name: Copy Config - run: cp .config/example.yml .config/default.yml - - name: Build - run: pnpm build - - name: Generate API JSON - run: pnpm --filter backend generate-api-json - - # build cherrypick js - - name: Build cherrypick-js - run: |- - cp packages/backend/built/api.json packages/cherrypick-js/generator/api.json - pnpm run --filter cherrypick-js-type-generator generate - - # packages/cherrypick-js/generator/built/autogen - - name: Upload Generated - uses: actions/upload-artifact@v4 - with: - name: generated-cherrypick-js - path: packages/cherrypick-js/generator/built/autogen - - # pull_request_target safety: permissions: read-all, and there are no secrets used in this job - get-actual-cherrypick-js: - runs-on: ubuntu-latest - permissions: - contents: read - if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} - steps: - - name: checkout - uses: actions/checkout@v4.1.1 - with: - submodules: true - ref: refs/pull/${{ github.event.pull_request.number }}/merge - - - name: Upload From Merged - uses: actions/upload-artifact@v4 - with: - name: actual-cherrypick-js - path: packages/cherrypick-js/src/autogen - - # pull_request_target safety: nothing is cloned from repository - comment-cherrypick-js-autogen: - runs-on: ubuntu-latest - needs: [generate-cherrypick-js, get-actual-cherrypick-js] - permissions: - pull-requests: write - steps: - - name: download generated-cherrypick-js - uses: actions/download-artifact@v4 - with: - name: generated-cherrypick-js - path: cherrypick-js-generated - - - name: download actual-cherrypick-js - uses: actions/download-artifact@v4 - with: - name: actual-cherrypick-js - path: cherrypick-js-actual - - - name: check cherrypick-js changes - id: check-changes - run: | - diff -r -u --label=generated --label=on-tree ./cherrypick-js-generated ./cherrypick-js-actual > cherrypick-js.diff || true - - if [ -s cherrypick-js.diff ]; then - echo "changes=true" >> $GITHUB_OUTPUT - else - echo "changes=false" >> $GITHUB_OUTPUT - fi - - - name: Print full diff - run: cat ./cherrypick-js.diff - - - name: send message - if: steps.check-changes.outputs.changes == 'true' - uses: thollander/actions-comment-pull-request@v2 - with: - comment_tag: check-cherrypick-js-autogen - message: |- - Thank you for sending us a great Pull Request! 👍 - Please regenerate cherrypick-js type definitions! 🙏 - - example: - ```sh - pnpm run build-cherrypick-js-with-types - ``` - - - name: send message - if: steps.check-changes.outputs.changes == 'false' - uses: thollander/actions-comment-pull-request@v2 - with: - comment_tag: check-cherrypick-js-autogen - mode: delete - message: "Thank you!" - create_if_not_exists: false diff --git a/.github/workflows/check-cherrypick-js-version.yml b/.github/workflows/check-cherrypick-js-version.yml deleted file mode 100644 index c508380e95..0000000000 --- a/.github/workflows/check-cherrypick-js-version.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Check CherryPick JS version - -on: - push: - branches: [ develop ] - paths: - - packages/cherrypick-js/package.json - - package.json - - .github/workflows/check-cherrypick-js-version.yml - pull_request: - branches: [ develop ] - paths: - - packages/cherrypick-js/package.json - - package.json - - .github/workflows/check-cherrypick-js-version.yml -jobs: - check-version: - # ルートの package.json と packages/cherrypick-js/package.json のバージョンが一致しているかを確認する - name: Check version - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - name: Check version - run: | - if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/cherrypick-js/package.json)" ]; then - echo "Version mismatch!" - exit 1 - fi diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml deleted file mode 100644 index 170150281b..0000000000 --- a/.github/workflows/check-spdx-license-id.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Check SPDX-License-Identifier - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - check-spdx-license-id: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - name: Check - run: | - counter=0 - - search() { - local directory="$1" - find "$directory" -type f \ - '(' \ - -name "*.cjs" -and -not -name '*.config.cjs' -o \ - -name "*.html" -o \ - -name "*.js" -and -not -name '*.config.js' -o \ - -name "*.mjs" -and -not -name '*.config.mjs' -o \ - -name "*.scss" -o \ - -name "*.ts" -and -not -name '*.config.ts' -o \ - -name "*.vue" \ - ')' -and \ - -not -name '*eslint*' - } - - check() { - local file="$1" - if ! ( - grep -q "SPDX-FileCopyrightText: syuilo and misskey-project" "$file" || - grep -q "SPDX-License-Identifier: AGPL-3.0-only" "$file" - ); then - echo "Missing: $file" - ((counter++)) - fi - } - - directories=( - "cypress/e2e" - "packages/backend/migration" - "packages/backend/src" - "packages/backend/test" - "packages/frontend/.storybook" - "packages/frontend/@types" - "packages/frontend/lib" - "packages/frontend/public" - "packages/frontend/src" - "packages/frontend/test" - "packages/misskey-bubble-game/src" - "packages/sw/src" - "scripts" - ) - - for directory in "${directories[@]}"; do - for file in $(search $directory); do - check "$file" - done - done - - if [ $counter -gt 0 ]; then - echo "SPDX-License-Identifier is missing in $counter files." - exit 1 - else - echo "SPDX-License-Identifier is certainly described in all target files!" - exit 0 - fi diff --git a/.github/workflows/deploy-test-environment.yml b/.github/workflows/deploy-test-environment.yml deleted file mode 100644 index 66b15beb91..0000000000 --- a/.github/workflows/deploy-test-environment.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: deploy-test-environment - -on: - issue_comment: - types: [created] - workflow_dispatch: - inputs: - repository: - description: 'Repository to deploy (optional, use the repository where this workflow is stored by default)' - required: false - default: '' - branch_or_hash: - description: 'Branch or Commit hash to deploy (optional, use the branch where this workflow is stored by default)' - required: false - default: '' - wait_time: - description: 'Time to wait in seconds (optional, 1800 seconds by default)' - required: false - default: '' - -jobs: - get-pr-ref: - runs-on: ubuntu-latest - if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/preview') - outputs: - is-allowed-user: ${{ steps.check-allowed-users.outputs.is-allowed-user }} - pr-ref: ${{ steps.get-ref.outputs.pr-ref }} - wait_time: ${{ steps.get-wait-time.outputs.wait_time }} - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - - name: Check allowed users - id: check-allowed-users - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ORG_ID: ${{ github.repository_owner_id }} - COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - run: | - MEMBERSHIP_STATUS=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/organizations/$ORG_ID/public_members/$COMMENT_AUTHOR" \ - -o /dev/null -w '%{http_code}\n' -s) - if [ "$MEMBERSHIP_STATUS" -eq 204 ]; then - echo "is-allowed-user=true" > $GITHUB_OUTPUT - else - echo "is-allowed-user=false" > $GITHUB_OUTPUT - fi - - - name: Get PR ref - id: get-ref - run: | - PR_REF="refs/pull/${{ github.event.issue.number }}/head" - echo "pr-ref=$PR_REF" >> $GITHUB_OUTPUT - - - name: Extract wait time - id: get-wait-time - env: - COMMENT_BODY: ${{ github.event.comment.body }} - run: | - WAIT_TIME=$(echo "$COMMENT_BODY" | grep -oP '(?<=/preview\s)\d+' || echo "1800") - echo "wait_time=$WAIT_TIME" > $GITHUB_OUTPUT - - deploy-test-environment-pr-comment: - needs: get-pr-ref - if: needs.get-pr-ref.outputs.is-allowed-user == 'true' - uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main - with: - repository: ${{ github.repository }} - branch_or_hash: ${{ needs.get-pr-ref.outputs.pr-ref }} - wait_time: ${{ needs.get-pr-ref.outputs.wait_time }} - secrets: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - - deploy-test-environment-wd: - if: github.event_name == 'workflow_dispatch' - uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main - with: - repository: ${{ inputs.repository || github.repository }} - branch_or_hash: ${{ inputs.branch_or_hash || github.ref_name }} - wait_time: ${{ inputs.wait_time || '1800' }} - secrets: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index e881ff2243..860100d277 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -6,20 +6,10 @@ on: - develop workflow_dispatch: -env: - REGISTRY_IMAGE: noridev/cherrypick - jobs: - # see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners - build: - name: Build + push_to_registry: + name: Push Docker image to Docker Hub runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - platform: - - linux/amd64 - - linux/arm64 if: github.repository == 'kokonect-link/cherrypick' steps: - name: Free Disk Space @@ -33,67 +23,32 @@ jobs: haskell: true large-packages: true swap-storage: true - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo uses: actions/checkout@v4.1.1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + id: buildx + uses: docker/setup-buildx-action@v3.0.0 + with: + platforms: linux/amd64,linux/arm64 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: noridev/cherrypick - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push by digest - id: build - uses: docker/build-push-action@v6 + - name: Build and Push to Docker Hub + uses: docker/build-push-action@v5 with: + builder: ${{ steps.buildx.outputs.name }} context: . push: true - platforms: ${{ matrix.platform }} + platforms: ${{ steps.buildx.outputs.platforms }} provenance: false + tags: noridev/cherrypick:develop labels: develop cache-from: type=gha cache-to: type=gha,mode=max - outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge: - runs-on: ubuntu-latest - needs: - - build - steps: - - name: Download digests - uses: actions/download-artifact@v4 - with: - path: /tmp/digests - pattern: digests-* - merge-multiple: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create --tag ${{ env.REGISTRY_IMAGE }}:develop \ - $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:develop diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1643aa380c..0bd12cb2c5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,27 +5,11 @@ on: types: [published] workflow_dispatch: -env: - REGISTRY_IMAGE: noridev/cherrypick - TAGS: | - type=edge - type=ref,event=pr - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - jobs: - # see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners - build: - name: Build + push_to_registry: + name: Push Docker image to Docker Hub runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - platform: - - linux/amd64 - - linux/arm64 + steps: - name: Free Disk Space uses: jlumbroso/free-disk-space@main @@ -38,79 +22,39 @@ jobs: haskell: true large-packages: true swap-storage: true - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo uses: actions/checkout@v4.1.1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + id: buildx + uses: docker/setup-buildx-action@v3.0.0 + with: + platforms: linux/amd64,linux/arm64 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY_IMAGE }} - tags: ${{ env.TAGS }} + images: noridev/cherrypick + tags: | + type=edge + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v5 with: + builder: ${{ steps.buildx.outputs.name }} context: . push: true - platforms: ${{ matrix.platform }} + platforms: ${{ steps.buildx.outputs.platforms }} provenance: false + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge: - runs-on: ubuntu-latest - needs: - - build - steps: - - name: Download digests - uses: actions/download-artifact@v4 - with: - path: /tmp/digests - pattern: digests-* - merge-multiple: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_IMAGE }} - tags: ${{ env.TAGS }} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index 483d679af2..bd130b9735 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -13,16 +13,14 @@ jobs: runs-on: ubuntu-latest env: DOCKER_CONTENT_TRUST: 1 - DOCKLE_VERSION: 0.4.14 steps: - uses: actions/checkout@v4.1.1 - - name: Download and install dockle v${{ env.DOCKLE_VERSION }} - run: | - curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb" + - run: | + curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" sudo dpkg -i dockle.deb - run: | cp .config/docker_example.env .config/docker.env - cp ./compose_example.yml ./compose.yml + cp ./docker-compose_example.yml ./docker-compose.yml - run: | docker compose up -d web docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" cherrypick-web:latest diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 6599d1dfd6..f16123ad79 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -9,6 +9,7 @@ on: paths: - packages/backend/** - .github/workflows/get-api-diff.yml + jobs: get-from-cherrypick: runs-on: ubuntu-latest @@ -17,7 +18,7 @@ jobs: strategy: matrix: - node-version: [20.16.0] + node-version: [20.10.0] api-json-name: [api-base.json, api-head.json] include: - api-json-name: api-base.json @@ -31,9 +32,12 @@ jobs: ref: ${{ matrix.ref }} submodules: true - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3d4ded680e..00fb665278 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,21 +5,8 @@ on: branches: - master - develop - paths: - - packages/backend/** - - packages/frontend/** - - packages/sw/** - - packages/cherrypick-js/** - - packages/shared/eslint.config.js - - .github/workflows/lint.yml pull_request: - paths: - - packages/backend/** - - packages/frontend/** - - packages/sw/** - - packages/cherrypick-js/** - - packages/shared/eslint.config.js - - .github/workflows/lint.yml + jobs: pnpm_install: runs-on: ubuntu-latest @@ -28,8 +15,11 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.0.3 + - uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - uses: actions/setup-node@v4.0.1 with: node-version-file: '.node-version' cache: 'pnpm' @@ -40,8 +30,6 @@ jobs: needs: [pnpm_install] runs-on: ubuntu-latest continue-on-error: true - env: - eslint-cache-version: v1 strategy: matrix: workspace: @@ -54,40 +42,17 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.0.3 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - run: pnpm --filter ${{ matrix.workspace }} run lint - - format: - needs: [pnpm_install] - runs-on: ubuntu-latest - continue-on-error: true - strategy: - matrix: - workspace: - - backend - - frontend - - cherrypick-js - - sw - steps: - - uses: actions/checkout@v4.1.1 + - uses: pnpm/action-setup@v2 with: - fetch-depth: 0 - submodules: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.0.2 + version: 7 + run_install: false + - uses: actions/setup-node@v4.0.1 with: node-version-file: '.node-version' cache: 'pnpm' - run: corepack enable - run: pnpm i --frozen-lockfile - - run: pnpm --filter ${{ matrix.workspace }} run format - + - run: pnpm --filter ${{ matrix.workspace }} run eslint typecheck: needs: [pnpm_install] @@ -103,8 +68,11 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.0.3 + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v4.0.1 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml deleted file mode 100644 index 95251bfe31..0000000000 --- a/.github/workflows/locale.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Lint - -on: - push: - paths: - - locales/** - - .github/workflows/locale.yml - pull_request: - paths: - - locales/** - - .github/workflows/locale.yml -jobs: - locale_verify: - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4.1.1 - with: - fetch-depth: 0 - submodules: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.0.3 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - run: cd locales && node verify.js diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml index 8362c006eb..c02b980e4d 100644 --- a/.github/workflows/ok-to-test.yml +++ b/.github/workflows/ok-to-test.yml @@ -23,7 +23,7 @@ jobs: private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v4 + uses: peter-evans/slash-command-dispatch@v3 env: TOKEN: ${{ steps.generate_token.outputs.token }} with: diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml deleted file mode 100644 index b6bb62df12..0000000000 --- a/.github/workflows/on-release-created.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: On Release Created (Publish cherrypick-js) - -on: - release: - types: [created] - - workflow_dispatch: - -jobs: - publish-cherrypick-js: - name: Publish cherrypick-js - runs-on: ubuntu-latest - - permissions: - contents: read - id-token: write - - strategy: - matrix: - node-version: [20.16.0] - - steps: - - uses: actions/checkout@v4.1.1 - with: - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - registry-url: 'https://registry.npmjs.org' - - name: Publish package - run: | - corepack enable - pnpm i --frozen-lockfile - pnpm build - pnpm --filter cherrypick-js publish --access public --no-git-checks --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} - NPM_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml index 28fcb87063..bb503055e9 100644 --- a/.github/workflows/pr-preview-deploy.yml +++ b/.github/workflows/pr-preview-deploy.yml @@ -13,7 +13,7 @@ jobs: github.event.client_payload.slash_command.sha != '' && contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) steps: - - uses: actions/github-script@v7.0.1 + - uses: actions/github-script@v7 id: check-id env: number: ${{ github.event.client_payload.pull_request.number }} @@ -37,7 +37,7 @@ jobs: return check[0].id; - - uses: actions/github-script@v7.0.1 + - uses: actions/github-script@v7 env: check_id: ${{ steps.check-id.outputs.result }} details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} @@ -72,7 +72,7 @@ jobs: timeout: 15m # Update check run called "integration-fork" - - uses: actions/github-script@v7.0.1 + - uses: actions/github-script@v7 id: update-check-run if: ${{ always() }} env: diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml index d2ad02b9ef..45a1b88bbe 100644 --- a/.github/workflows/pr-preview-destroy.yml +++ b/.github/workflows/pr-preview-destroy.yml @@ -10,7 +10,7 @@ jobs: destroy-preview-environment: runs-on: ubuntu-latest steps: - - uses: actions/github-script@v7.0.1 + - uses: actions/github-script@v7 id: check-conclusion env: number: ${{ github.event.number }} diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml deleted file mode 100644 index f86c1948f8..0000000000 --- a/.github/workflows/release-edit-with-push.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: "Release Manager: sync changelog with PR" - -on: - push: - branches: - - develop - paths: - - 'CHANGELOG.md' - # - .github/workflows/release-edit-with-push.yml -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - edit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得 - - name: Get PR - run: | - echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT - id: get_pr - env: - STABLE_BRANCH: ${{ vars.STABLE_BRANCH }} - - name: Get target version - if: steps.get_pr.outputs.pr_number != '' - uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2 - id: v - # CHANGELOG.mdの内容を取得 - - name: Get changelog - if: steps.get_pr.outputs.pr_number != '' - uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2 - with: - version: ${{ steps.v.outputs.target_version }} - id: changelog - # PRのnotesを更新 - - name: Update PR - if: steps.get_pr.outputs.pr_number != '' - run: | - gh pr edit "$PR_NUMBER" --body "$CHANGELOG" - env: - PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }} - CHANGELOG: ${{ steps.changelog.outputs.changelog }} diff --git a/.github/workflows/release-with-dispatch.yml b/.github/workflows/release-with-dispatch.yml deleted file mode 100644 index 0936bc0ae8..0000000000 --- a/.github/workflows/release-with-dispatch.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: "Release Manager [Dispatch]" - -on: - workflow_dispatch: - inputs: - ## Specify the type of the next release. - #version_increment_type: - # type: choice - # description: 'VERSION INCREMENT TYPE' - # default: 'patch' - # required: false - # options: - # - 'major' - # - 'minor' - # - 'patch' - merge: - type: boolean - description: 'MERGE RELEASE BRANCH TO MAIN' - default: false - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - get-pr: - runs-on: ubuntu-latest - outputs: - pr_number: ${{ steps.get_pr.outputs.pr_number }} - steps: - - uses: actions/checkout@v4 - # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得 - - name: Get PRs - run: | - echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT - id: get_pr - env: - STABLE_BRANCH: ${{ vars.STABLE_BRANCH }} - - merge: - uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }} - with: - pr_number: ${{ needs.get-pr.outputs.pr_number }} - user: 'github-actions[bot]' - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - # Text to prepend to the changelog - # The first line must be `## Unreleased` - changes_template: | - ## Unreleased - - ### General - - - - ### Client - - - - ### Server - - - - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - create-prerelease: - uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }} - with: - pr_number: ${{ needs.get-pr.outputs.pr_number }} - user: 'github-actions[bot]' - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - create-target: - uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number == '' }} - with: - user: 'github-actions[bot]' - # The script for version increment. - # process.env.CURRENT_VERSION: The current version. - # - # Misskey calender versioning (yyyy.MM.patch) example - version_increment_script: | - const now = new Date(); - const year = now.toLocaleDateString('en-US', { year: 'numeric', timeZone: 'Asia/Tokyo' }); - const month = now.toLocaleDateString('en-US', { month: 'numeric', timeZone: 'Asia/Tokyo' }); - const [major, minor, _patch] = process.env.CURRENT_VERSION.split('.'); - const patch = Number(_patch.split('-')[0]); - if (Number.isNaN(patch)) { - console.error('Invalid patch version', year, month, process.env.CURRENT_VERSION, major, minor, _patch); - throw new Error('Invalid patch version'); - } - if (year !== major || month !== minor) { - return `${year}.${month}.0`; - } else { - return `${major}.${minor}.${patch + 1}`; - } - ##Semver example - #version_increment_script: | - # const [major, minor, patch] = process.env.CURRENT_VERSION.split('.'); - # if ("${{ inputs.version_increment_type }}" === "major") { - # return `${Number(major) + 1}.0.0`; - # } else if ("${{ inputs.version_increment_type }}" === "minor") { - # return `${major}.${Number(minor) + 1}.0`; - # } else { - # return `${major}.${minor}.${Number(patch) + 1}`; - # } - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - stable_branch: ${{ vars.STABLE_BRANCH }} - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release-with-ready.yml b/.github/workflows/release-with-ready.yml deleted file mode 100644 index 79b6ade012..0000000000 --- a/.github/workflows/release-with-ready.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: "Release Manager: release RC when ready for review" - -on: - pull_request: - types: [ready_for_review] - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - check: - runs-on: ubuntu-latest - outputs: - head: ${{ steps.get_pr.outputs.head }} - base: ${{ steps.get_pr.outputs.base }} - steps: - - uses: actions/checkout@v4 - # PR情報を取得 - - name: Get PR - run: | - pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName) - echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT - echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT - id: get_pr - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - release: - uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2 - needs: check - if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH - with: - pr_number: ${{ github.event.pull_request.number }} - user: 'github-actions[bot]' - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml index 7ec2aae3ce..2c7df3fc7b 100644 --- a/.github/workflows/report-api-diff.yml +++ b/.github/workflows/report-api-diff.yml @@ -16,7 +16,7 @@ jobs: # api-artifact steps: - name: Download artifact - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v7 with: script: | const fs = require('fs'); diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml deleted file mode 100644 index 4396e96f3f..0000000000 --- a/.github/workflows/scorecard.yml +++ /dev/null @@ -1,73 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. They are provided -# by a third-party and are governed by separate terms of service, privacy -# policy, and support documentation. - -name: Scorecard supply-chain security -on: - # For Branch-Protection check. Only the default branch is supported. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection - branch_protection_rule: - # To guarantee Maintained check is occasionally updated. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained - schedule: - - cron: '26 20 * * 5' - push: - branches: [ "develop" ] - -# Declare default permissions as read only. -permissions: read-all - -jobs: - analysis: - name: Scorecard analysis - runs-on: ubuntu-latest - permissions: - # Needed to upload the results to code-scanning dashboard. - security-events: write - # Needed to publish results and get a badge (see publish_results below). - id-token: write - # Uncomment the permissions below if installing in a private repository. - # contents: read - # actions: read - - steps: - - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - persist-credentials: false - - - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 - with: - results_file: results.sarif - results_format: sarif - # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: - # - you want to enable the Branch-Protection check on a *public* repository, or - # - you are installing Scorecard on a *private* repository - # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. - # repo_token: ${{ secrets.SCORECARD_TOKEN }} - - # Public repositories: - # - Publish results to OpenSSF REST API for easy access by consumers - # - Allows the repository to include the Scorecard badge. - # - See https://github.com/ossf/scorecard-action#publishing-results. - # For private repositories: - # - `publish_results` will always be set to `false`, regardless - # of the value entered here. - publish_results: true - - # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF - # format to the repository Actions tab. - - name: "Upload artifact" - uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - # Upload the results to GitHub's code scanning dashboard (optional). - # Commenting out will disable upload of results to your repo's Code Scanning dashboard - - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 - with: - sarif_file: results.sarif diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml deleted file mode 100644 index cf98e5236f..0000000000 --- a/.github/workflows/storybook.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Storybook - -on: - push: - branches: - - master - - develop - - dev/storybook8 # for testing - pull_request_target: - -jobs: - build: - runs-on: ubuntu-latest - - env: - NODE_OPTIONS: "--max_old_space_size=7168" - - steps: - - uses: actions/checkout@v4.1.1 - if: github.event_name != 'pull_request_target' - with: - fetch-depth: 0 - submodules: true - - uses: actions/checkout@v4.1.1 - if: github.event_name == 'pull_request_target' - with: - fetch-depth: 0 - submodules: true - ref: "refs/pull/${{ github.event.number }}/merge" - - name: Checkout actual HEAD - if: github.event_name == 'pull_request_target' - id: rev - run: | - echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT - git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3) - - name: Install pnpm - uses: pnpm/action-setup@v4 - - name: Use Node.js 20.x - uses: actions/setup-node@v4.0.3 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - name: Build cherrypick-js - run: pnpm --filter cherrypick-js build - - name: Build storybook - run: pnpm --filter frontend build-storybook - - name: Publish to Chromatic - if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master' - run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static - env: - CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - - name: Publish to Chromatic - if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master' - id: chromatic_push - run: | - DIFF="${{ github.event.before }} HEAD" - if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then - DIFF="HEAD" - fi - CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" - if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - fi - if pnpm --filter frontend chromatic -d storybook-static $(echo "$CHROMATIC_PARAMETER"); then - echo "success=true" >> $GITHUB_OUTPUT - else - echo "success=false" >> $GITHUB_OUTPUT - fi - env: - CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - - name: Publish to Chromatic - if: github.event_name == 'pull_request_target' - id: chromatic_pull_request - run: | - DIFF="${{ steps.rev.outputs.base }} HEAD" - if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then - DIFF="HEAD" - fi - CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" - if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - fi - BRANCH="${{ github.event.pull_request.head.user.login }}:$HEAD_REF" - if [ "$BRANCH" = "kokonect-link:$HEAD_REF" ]; then - BRANCH="$HEAD_REF" - fi - pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name "$BRANCH" $(echo "$CHROMATIC_PARAMETER") - env: - HEAD_REF: ${{ github.event.pull_request.head.ref }} - CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - - name: Notify that Chromatic detects changes - uses: actions/github-script@v7.0.1 - if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.repos.createCommitComment({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: context.sha, - body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).' - }) - - name: Upload Artifacts - uses: actions/upload-artifact@v4 - with: - name: storybook - path: packages/frontend/storybook-static diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 5b53bfa0bd..a465883a0d 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -5,24 +5,15 @@ on: branches: - master - develop - paths: - - packages/backend/** - # for permissions - - packages/cherrypick-js/** - - .github/workflows/test-backend.yml pull_request: - paths: - - packages/backend/** - # for permissions - - packages/cherrypick-js/** - - .github/workflows/test-backend.yml + jobs: - unit: + jest: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.16.0] + node-version: [20.10.0] services: postgres: @@ -42,11 +33,12 @@ jobs: with: submodules: true - name: Install pnpm - uses: pnpm/action-setup@v4 - - name: Install FFmpeg - uses: FedericoCarboni/setup-ffmpeg@v3 + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -59,56 +51,9 @@ jobs: - name: Build run: pnpm build - name: Test - run: pnpm --filter backend test-and-coverage - - name: Upload to Codecov - uses: codecov/codecov-action@v4 + run: pnpm jest-and-coverage + - name: Upload Coverage + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./packages/backend/coverage/coverage-final.json - - e2e: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.16.0] - - services: - postgres: - image: postgres:15 - ports: - - 54312:5432 - env: - POSTGRES_DB: test-cherrypick - POSTGRES_HOST_AUTH_METHOD: trust - redis: - image: redis:7 - ports: - - 56312:6379 - - steps: - - uses: actions/checkout@v4.1.1 - with: - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - name: Copy Configure - run: cp .github/cherrypick/test.yml .config - - name: Build - run: pnpm build - - name: Test - run: pnpm --filter backend test-and-coverage:e2e - - name: Upload to Codecov - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./packages/backend/coverage/coverage-final.json diff --git a/.github/workflows/test-cherrypick-js.yml b/.github/workflows/test-cherrypick-js.yml index 510910eb18..f693f0355d 100644 --- a/.github/workflows/test-cherrypick-js.yml +++ b/.github/workflows/test-cherrypick-js.yml @@ -6,14 +6,9 @@ name: Test (cherrypick.js) on: push: branches: [ develop ] - paths: - - packages/cherrypick-js/** - - .github/workflows/test-cherrypick-js.yml pull_request: branches: [ develop ] - paths: - - packages/cherrypick-js/** - - .github/workflows/test-cherrypick-js.yml + jobs: test: @@ -21,7 +16,7 @@ jobs: strategy: matrix: - node-version: [20.16.0] + node-version: [20.10.0] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: @@ -31,7 +26,7 @@ jobs: - run: corepack enable - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -51,7 +46,7 @@ jobs: CI: true - name: Upload Coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./packages/cherrypick-js/coverage/coverage-final.json diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index c7cc36f03e..ab96fb5c28 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -5,37 +5,27 @@ on: branches: - master - develop - paths: - - packages/frontend/** - # for permissions - - packages/cherrypick-js/** - # for e2e - - packages/backend/** - - .github/workflows/test-frontend.yml pull_request: - paths: - - packages/frontend/** - # for permissions - - packages/cherrypick-js/** - # for e2e - - packages/backend/** - - .github/workflows/test-frontend.yml + jobs: vitest: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.16.0] + node-version: [20.10.0] steps: - uses: actions/checkout@v4.1.1 with: submodules: true - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -50,7 +40,7 @@ jobs: - name: Test run: pnpm --filter frontend test-and-coverage - name: Upload Coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./packages/frontend/coverage/coverage-final.json @@ -61,7 +51,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.16.0] + node-version: [20.10.0] browser: [chrome] services: @@ -88,9 +78,12 @@ jobs: #- uses: browser-actions/setup-firefox@latest # if: ${{ matrix.browser == 'firefox' }} - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -109,7 +102,6 @@ jobs: run: pnpm exec cypress install - name: Cypress run uses: cypress-io/github-action@v6 - timeout-minutes: 15 with: install: false start: pnpm start:test diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index bea84cee35..91f348600b 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -16,16 +16,19 @@ jobs: strategy: matrix: - node-version: [20.16.0] + node-version: [20.10.0] steps: - uses: actions/checkout@v4.1.1 with: submodules: true - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml deleted file mode 100644 index 06e987f27e..0000000000 --- a/.github/workflows/validate-api-json.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Test (backend) - -on: - push: - branches: - - master - - develop - paths: - - packages/backend/** - - .github/workflows/validate-api-json.yml - pull_request: - paths: - - packages/backend/** - - .github/workflows/validate-api-json.yml -jobs: - validate-api-json: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.16.0] - - steps: - - uses: actions/checkout@v4.1.1 - with: - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - name: Install Redocly CLI - run: npm i -g @redocly/cli - - run: corepack enable - - run: pnpm i --frozen-lockfile - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - name: Copy Configure - run: cp .config/example.yml .config/default.yml - - name: Build and generate - run: pnpm build && pnpm --filter backend generate-api-json - - name: Validation - run: npx @redocly/cli lint --extends=minimal ./packages/backend/built/api.json diff --git a/.gitignore b/.gitignore index 531fb35d11..74a0bb3c04 100644 --- a/.gitignore +++ b/.gitignore @@ -35,13 +35,12 @@ coverage !/.config/example.yml !/.config/docker_example.yml !/.config/docker_example.env -.devcontainer/compose.yml -!/.devcontainer/compose.yml +docker-compose.yml +!/.devcontainer/docker-compose.yml # cherrypick /build built -built-test /data /.cache-loader /db @@ -58,8 +57,6 @@ api-docs.json ormconfig.json temp /packages/frontend/src/**/*.stories.ts -tsdoc-metadata.json -misskey-assets # blender backups *.blend1 diff --git a/.gitmodules b/.gitmodules index 3218575273..225a69a652 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ +[submodule "misskey-assets"] + path = misskey-assets + url = https://github.com/misskey-dev/assets.git [submodule "fluent-emojis"] path = fluent-emojis url = https://github.com/misskey-dev/emojis.git diff --git a/.node-version b/.node-version index 8ce7030825..d5a159609d 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.16.0 +20.10.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3cdf81e339..baca8db246 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,9 @@ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", + "Vue.vscode-typescript-vue-plugin", "Orta.vscode-jest", + "dbaeumer.vscode-eslint", "mrmlnc.vscode-json5" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ceec23acd..e2a82b1ffe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,7 @@ "*.test.ts": "typescript" }, "jest.jestCommandLine": "pnpm run jest", - "jest.runMode": "on-demand", + "jest.autoRun": "off", "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index dfded3626a..87b25078c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,393 +1,22 @@ -## Unreleased + ## 2023.12.2 ### General - v2023.12.1でDockerを利用してサーバーを起動できない問題を修正 -### Client -- Enhance: 検索画面においてEnterキー押下で検索できるように - ## 2023.12.1 ### Note @@ -495,6 +124,7 @@ - Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように - Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正 - Fix: MFMでルビの中のテキストがnyaizeされない問題を修正 +- Enhance: 検索画面においてEnterキー押下で検索できるように ### Server - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md index ff5d49caea..15f28deb5d 100644 --- a/CHANGELOG_CHERRYPICK.md +++ b/CHANGELOG_CHERRYPICK.md @@ -1,8 +1,8 @@ -## x.x.x (unreleased) - -### Release Date - -### General -- リバーシは削除されました -- センシティブなユーザーをHTL/LTL/STL上で表示しないようになりました -- 管理者側でユーザーがセンシティブかどうか設定できるようになりました - - それに伴い、ユーザー側で設定できなくなりました - -### Client -- 検索ウィジェットにオートフォーカスが当たらなくなりました -- 画像がAPNGの場合GIFではなくAPNGと表示するように -- 隠すボタンのスタイル調整 -- ファイル名とALTを同時に表示するように -- ALTテキストが存在する場合投稿フォームのファイルにインジケータを表示するように -- ALTテキストがない場合警告を出すオプションを追加 -- 画像のタイトルには必ずファイル名を使用するように -- fix: クロッパーで画像の全体が表示されないことがある問題 - -### Server -- 検索機能に新しいパラメータを追加( [1673beta/cherrypick#94](https://github.com/1673beta/cherrypick/issues/94) ) - -### Misc - - - -## 0.4.6 - -### Release Date - -### General -- チャット機能は非推奨になりました - - 将来的に廃止する予定です - -### Client -- モバイルUIにおいて、下部のナビゲーションバーに表示されるチャットページへの誘導を検索に置き換え -- feat: 検索ウィジェット - -### Server -- - -### Misc - - -## 0.4.5 - -### Release Date - -### General -- `follow-me`は廃止されました -- CherryPick 4.10.0-beta.2に追従しました -- ユーザーにセンシティブフラグが付けられるようになりました - - 有効にすると、投稿は自動的にCWがつくようになります -- `blocking/create`でブロックした際、同時にミュートするようになりました - - `blocking/delete`をしてもミュートは解除されません。 - -### Client -- Botからのフォローを承認制にすることができるようになりました - - Botフラグがついていない人からのフォローは自動承認となります - -### Server -- isIndexableがfalseなユーザーはアンテナに載らないように - -### Misc - -## 0.4.4 - -### Release Date - -### General -- CherryPick 4.10.0-beta.1に追従 - -### Client -- Deck UIのサーバーアイコンのスタイルを調整 - -### Server -- - -### Misc - - -## 0.4.3 - -### Release Date - -### General -- パスワードのハッシュでArgon2idをサポート - - bcryptによるハッシュは再ログイン時にArgon2idに置き換えられます -- `notes/advanced-search`を廃止 -- `notes/search`でmeilisearchを利用するのを廃止 - - リソースを多く消費してしまうことと、日本語全文検索に向いていないため -- 新しい公開範囲「プライベート」を追加 - - 投稿者しか見れません - -### Client -- fix: URLスキーマのチェックが不完全だった問題 - -### Server -- feat: isIndexableプロパティ - - 「設定>プライバシー>「公開ノートをインデックス化」」をオフにすると、あなたのノートが検索に引っ掛からなくなります - -### Misc - -## 0.4.2 - -### Release Date - -### General -- Vueのバージョンをアップデート - - 画面に何も表示されなくなるバグが修正 - -### Client -- Botの投稿をタイムラインから除外できるように -- Pageの可読性を向上 - -### Server -- - -### Misc - - - -## 0.4.1 - -### Release Date - -### General -- ノートの自動削除機能を追加 - -### Client -- ダイスウィジェットを追加 -- 通知を全て削除できるボタンを追加 -- 非ログイン時にブロック/配送停止/サイレンスしているサーバーが見れないように - -### Server -- 管理者アカウントを別サーバーに移行できるように -- APIドキュメントをRedocからscalarにして軽量化 -- fix: OAuthにレートリミットがかかっていない問題 -- fix: SQLエスケープが不完全な問題 -- feat: 通知を個別削除するAPI - -### Misc - - -## 0.4.0 - -### Release Date - -### General -- yojo-artフォークで実装された機能の取り込み - -### Client -- Catppuccin Themeをビルトインテーマとして利用可能に (https://github.com/catppuccin/misskey?tab=readme-ov-file) -- ユーザー、ノート、ギャラリー、ページをQRCodeで共有可能に -- オフライン画面のデザインを変更 -- ダークモードを切り替えるショートカットキーの変更 -- 検索boxを統一 - -### Server -- - -### Misc - - -## 0.3.1 - -### Release Date - -### General -- feat: 高度な検索機能の追加 - - PGroongaやOpenSearchを利用できるようになります - -### Client -- fix: 投稿にされたリアクションが正常に表示されない問題 - -### Server - -## 0.3.0 - -### Release Date -2024-03-14 - -### General -- aboutにステータスページを表示できるように - - コントロールパネル>設定>全般から設定できます - -### Client -- ノートの最大字数が5120文字に -- ロケール変更 -- 時限消滅ノートの追加 - -### Server -- - -### Misc -- devcontainerが起動しない問題を修正 - -## 0.2.0 - -### Release Date -2024-03-11 - -### General -- メディアタイムラインの実装 - - グローバルタイムラインを閲覧できる場合に閲覧できます - -### Misc -- CHANGELOG_engawaを昇順に修正 -- OthersをMiscに変更 - -## 0.1.1 - -### Release Date -2024-03-09 - -### Client -- ロケール変更 - -## 0.1.0 - -### Release Date -2024-03-09 - -### General -- アバターデコレーションの最大数を変更 -- ギャラリーページに載せられる画像の最大数を変更 -- ワードミュートの制限が文字数から個数に - -### Server -- リモートユーザーが凍結されている場合、ジョブをリトライしないように - -### Others -- フォークへの切り替え - - - - diff --git a/CHANGELOG_yojo.md b/CHANGELOG_yojo.md deleted file mode 100644 index e6ceb0c121..0000000000 --- a/CHANGELOG_yojo.md +++ /dev/null @@ -1,81 +0,0 @@ - -## 0.3.0 (unreleased) - -### Release Date - -### General -- - -### Client -- - -### Server -- Feat: OpenSearchを利用できるように -- Enhance: 高度な検索に新たな条件を追加(OpenSearchが必要です) - - 添付ファイルのセンシティブ条件(なし/含む/除外) - - 引用ノート除外 - - 検索方法の詳細はdoc/Advanced-Search.mdに -- Change:APIのパラメータを変更 - - notes/advanced-search の"excludeNsfw"を"excludeCW"に変更 - - notes/advanced-search の"channelId"を削除 - -## 0.2.2 -Cherrypick 4.9.0-beta.2 - -### General - -### Client - -### Server -- remove: チャンネル機能のAPIを削除 - -## 0.2.1 -Cherrypick 4.9.0-beta.2 - -### Client -- feat: マスコット画像を表示するウィジェットを追加 - -## 0.2.0 -Cherrypick 4.9.0-beta.2 - -### General -- enhance: ノートとユーザーの検索時に照会を行うかが選択できるようになりました - - @foo@example.com 形式でユーザ検索した場合に照会ができるようになりました -### Server -- fix: リモートユーザーにはファイルサイズ制限を適用しない - -## 0.1.0 (unreleased) - -### General -- enhance: メディアプロキシurlと拡大画像urlを分割 -- enhance: 1ファイルの容量をロールでも制限できるように - -### Client -- enhance: ノートとユーザーの検索時に照会を行うかが選択できるようになりました - - @foo​@example.com 形式でユーザ検索した場合に照会ができるようになりました -- add: 通知音を追加 [@mujin-nohuman (無人)](https://github.com/mujin-nohuman) -- fix: "キャッシュをクリア"してもインスタンス情報が更新されない不具合を修正 [#101](https://github.com/yojo-art/cherrypick/issues/101) - -### Server -- enhance: remoteProxyエンドポイント設定を追加 -- fix: webpublic生成時にドライブの縮小設定を見るように - -### Others -- engawaをマージ -- cherrypickからフォーク diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32d3210c64..2384ebc663 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contribution guide We're glad you're interested in contributing CherryPick! In this document you will find the information you need to contribute to the project. -> [!NOTE] +> **Note** > This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** > Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ > The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. @@ -17,31 +17,16 @@ Before creating an issue, please check the following: - Issues should only be used to feature requests, suggestions, and bug tracking. - Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/kokonect-link/cherrypick/discussions) or [Discord](https://discord.gg/V8qghB28Aj). -> [!WARNING] +> **Warning** > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. -### Recommended discussing before implementation -We welcome your proposal. - +## Before implementation When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them. PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review. -Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Committer to assign you). -By expressing your intention to work on the Issue, you can prevent conflicts in the work. - -To the Committers: you should not assign someone on it before the Final Decision. - -### How issues are triaged - -The Committers may: -* close an issue that is not reproducible on latest stable release, -* merge an issue into another issue, -* split an issue into multiple issues, -* or re-open that has been closed for some reason which is not applicable anymore. - -@syuilo reserves the Final Decision rights including whether the project will implement feature and how to implement, these rights are not always exercised. +Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work. ## Well-known branches - **`master`** branch is tracking the latest release and used for production purposes. @@ -52,14 +37,14 @@ The Committers may: ## Creating a PR Thank you for your PR! Before creating a PR, please check the following: - If possible, prefix the title with a keyword that identifies the type of this PR, as shown below. - - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc - - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. + - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc + - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. - If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. - Please add the summary of the changes to [`CHANGELOG_CHERRYPICK.md`](/CHANGELOG_CHERRYPICK.md). However, this is not necessary for changes that do not affect the users, such as refactoring. - Check if there are any documents that need to be created or updated due to this change. - If you have added a feature or fixed a bug, please add a test case if possible. - Please make sure that tests and Lint are passed in advance. - - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing) + - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing) - If this PR includes UI changes, please attach a screenshot in the text. Thanks for your cooperation 🤗 @@ -69,8 +54,8 @@ Be willing to comment on the good points and not just the things you want fixed ### Review perspective - Scope - - Are the goals of the PR clear? - - Is the granularity of the PR appropriate? + - Are the goals of the PR clear? + - Is the granularity of the PR appropriate? - Security - Does merging this PR create a vulnerability? - Performance @@ -92,7 +77,7 @@ An actual domain will be assigned so you can test the federation. ## Release ### Release Instructions -1. Commit version changes in the `develop` branch ([package.json](package.json)) +1. Commit version changes in the `develop` branch ([package.json](https://github.com/kokonect-link/cherrypick/blob/develop/package.json)) 2. Create a release PR. - Into `master` from `develop` branch. - The title must be in the format `Release: x.y.z`. @@ -103,7 +88,7 @@ An actual domain will be assigned so you can test the federation. - The target branch must be `master` - The tag name must be the version -> [!NOTE] +> **Note** > Why this instruction is necessary: > - To perform final QA checks > - To distribute responsibility @@ -121,24 +106,25 @@ If your language is not listed in Crowdin, please open an issue. ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) ## Development -### Setup -Before developing, you have to set up environment. CherryPick requires Redis, PostgreSQL, and FFmpeg. - -You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it. - -There are a few ways to proceed. +During development, it is useful to use the -#### Use system-wide software -You could install them in system-wide (such as from package manager). +``` +pnpm dev +``` -#### Use `docker compose` -You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`. +command. -#### Use Devcontainer -Devcontainer also has necessary setting. This method can be done by connecting from VSCode. +- 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. +### Dev Container Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. -To use Dev Container, open the project directory on VSCode with Dev Containers installed. +To use Dev Container, open the project directory on VSCode with Dev Containers installed. **Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled. It will run the following command automatically inside the container. @@ -150,35 +136,11 @@ pnpm build pnpm migrate ``` -After finishing the migration, you can proceed. +After finishing the migration, run the `pnpm dev` command to start the development server. -### Start developing -During development, it is useful to use the -``` +``` bash 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. - -### `CP_DEV_PREFER=backend pnpm dev` -pnpm dev has another mode with `CP_DEV_PREFER=backend`. - -``` -CP_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). -- You can see CherryPick 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 - Test codes are located in [`/packages/backend/test`](/packages/backend/test). @@ -190,7 +152,7 @@ cp .github/cherrypick/test.yml .config/ ``` Prepare DB/Redis for testing. ``` -docker compose -f packages/backend/test/compose.yml up +docker compose -f packages/backend/test/docker-compose.yml up ``` Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. @@ -229,7 +191,7 @@ niraxは、CherryPickで使用しているオリジナルのフロントエン ### ルート定義 ルート定義は、以下の形式のオブジェクトの配列です。 -```ts +``` ts { name?: string; path: string; @@ -242,7 +204,7 @@ niraxは、CherryPickで使用しているオリジナルのフロントエン } ``` -> [!WARNING] +> **Warning** > 現状、ルートは定義された順に評価されます。 > たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。 @@ -304,7 +266,7 @@ export const Default = { parameters: { layout: 'centered', }, -} satisfies StoryObj; +} satisfies StoryObj; ``` If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file. @@ -319,120 +281,30 @@ You can override the component meta by creating a meta story file (`MyComponent. ```ts export const argTypes = { scale: { - control: { - type: 'range', - min: 1, - max: 4, - }, - }, + control: { + type: 'range', + min: 1, + max: 4, + }, + }, }; ``` Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers. ```ts -import { HttpResponse, http } from 'msw'; +import { rest } from 'msw'; export const handlers = [ - http.post('/api/notes/timeline', ({ request }) => { - return HttpResponse.json([]); + rest.post('/api/notes/timeline', (req, res, ctx) => { + return res( + ctx.json([]), + ); }), ]; ``` Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files. -## Nest - -### Nest Service Circular dependency / Nestでサービスの循環参照でエラーが起きた場合 - -#### forwardRef -まずは簡単に`forwardRef`を試してみる - -```typescript -export class FooService { - constructor( - @Inject(forwardRef(() => BarService)) - private barService: BarService - ) { - } -} -``` - -#### OnModuleInit -できなければ`OnModuleInit`を使う - -```typescript -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; -import { BarService } from '@/core/BarService'; - -@Injectable() -export class FooService implements OnModuleInit { - private barService: BarService // constructorから移動してくる - - constructor( - private moduleRef: ModuleRef, - ) { - } - - async onModuleInit() { - this.barService = this.moduleRef.get(BarService.name); - } - - public async niceMethod() { - return await this.barService.incredibleMethod({ hoge: 'fuga' }); - } -} -``` - -##### Service Unit Test -テストで`onModuleInit`を呼び出す必要がある - -```typescript -// import ... - -describe('test', () => { - let app: TestingModule; - let fooService: FooService; // for test case - let barService: BarService; // for test case - - beforeEach(async () => { - app = await Test.createTestingModule({ - imports: ..., - providers: [ - FooService, - { // mockする (mockは必須ではないかもしれない) - provide: BarService, - useFactory: () => ({ - incredibleMethod: jest.fn(), - }), - }, - { // Provideにする - provide: BarService.name, - useExisting: BarService, - }, - ], - }) - .useMocker(... - .compile(); - - fooService = app.get(FooService); - barService = app.get(BarService) as jest.Mocked; - - // onModuleInitを実行する - await fooService.onModuleInit(); - }); - - test('nice', () => { - await fooService.niceMethod(); - - expect(barService.incredibleMethod).toHaveBeenCalled(); - expect(barService.incredibleMethod.mock.lastCall![0]) - .toEqual({ hoge: 'fuga' }); - }); -}) -``` - ## Notes ### Misskeyのドメイン固有の概念は`Mi`をprefixする @@ -539,13 +411,13 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o - 作成されたスクリプトは不必要な変更を含むため除去してください ### JSON SchemaのobjectでanyOfを使うとき -JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 -バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます) +JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 +バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます) https://github.com/misskey-dev/misskey/pull/10082 テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合: -```ts +``` export const paramDef = { type: 'object', properties: { diff --git a/COPYING b/COPYING index 53d498fb43..57ad466702 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2024 syuilo & noridev and contributors +Copyright © 2014-2024 syuilo and noridev and other misskey, cherrypick 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 1f6f7d0f43..75ff80d229 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=20.16.0-bullseye +ARG NODE_VERSION=20.10.0-bullseye # build assets & compile TypeScript @@ -24,15 +24,14 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/frontend/package.json", "./packages/frontend/"] COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/cherrypick-js/package.json", "./packages/cherrypick-js/"] -COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] - -ARG NODE_ENV=production RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output COPY --link . ./ +ARG NODE_ENV=production + RUN git submodule update --init RUN pnpm build RUN rm -rf .git/ @@ -53,9 +52,6 @@ COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] COPY --link ["scripts", "./scripts"] COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/cherrypick-js/package.json", "./packages/cherrypick-js/"] -COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] - -ARG NODE_ENV=production RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output @@ -80,17 +76,11 @@ RUN apt-get update \ USER cherrypick WORKDIR /cherrypick -# add package.json to add pnpm -COPY --chown=cherrypick:cherrypick ./package.json ./package.json -RUN corepack install - COPY --chown=cherrypick:cherrypick --from=target-builder /cherrypick/node_modules ./node_modules COPY --chown=cherrypick:cherrypick --from=target-builder /cherrypick/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=cherrypick:cherrypick --from=target-builder /cherrypick/packages/cherrypick-js/node_modules ./packages/cherrypick-js/node_modules -COPY --chown=cherrypick:cherrypick --from=target-builder /cherrypick/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules COPY --chown=cherrypick:cherrypick --from=native-builder /cherrypick/built ./built COPY --chown=cherrypick:cherrypick --from=native-builder /cherrypick/packages/cherrypick-js/built ./packages/cherrypick-js/built -COPY --chown=cherrypick:cherrypick --from=native-builder /cherrypick/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built COPY --chown=cherrypick:cherrypick --from=native-builder /cherrypick/packages/backend/built ./packages/backend/built COPY --chown=cherrypick:cherrypick --from=native-builder /cherrypick/fluent-emojis /cherrypick/fluent-emojis COPY --chown=cherrypick:cherrypick . ./ diff --git a/README.md b/README.md index 5ff2381b5f..839faf85b7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@
- CherryPick logo + CherryPick logo -**🌎 **CherryPick** is an open source, federated social media platform that's free forever! 🚀** - -[Learn more](https://misskey-hub.net/) +**🌎 **[CherryPick](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀** --- @@ -24,13 +22,44 @@ become a patron +--- + +[![codecov](https://codecov.io/gh/kokonect-link/cherrypick/branch/develop/graph/badge.svg?token=3BRDXE34O0)](https://codecov.io/gh/kokonect-link/cherrypick) +
-## Thanks +
+ + + +## ✨ Features +- **ActivityPub support**\ +Not on CherryPick? No problem! Not only can CherryPick instances talk to each other, but you can make friends with people on other networks like Mastodon and Misskey and Pixelfed! +- **Reactions**\ +You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button. +- **Drive**\ +With CherryPick's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made! +- **Rich Web UI**\ + CherryPick has a rich and easy to use Web UI! + It is highly customizable, from changing the layout and adding widgets to making custom themes. + Furthermore, plugins can be created using AiScript, an original programming language. +- And much more... + +
+ +
+ +## Documentation + +CherryPick Documentation can be found at [Misskey Hub](https://misskey-hub.net/docs/), some of the links and graphics above also lead to specific portions of it. -Sentry +## Sponsors -Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors. +
+ RSS3 +
+ +## Thanks Chromatic diff --git a/ROADMAP.md b/ROADMAP.md index 509ecb9fe7..3077c41e73 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,7 +6,6 @@ Also, the later tasks are more indefinite and are subject to change as developme This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development. - ~~Make the number of type errors zero (backend)~~ → Done ✔️ -- Make the number of type errors zero (frontend) - Improve CI - ~~Fix tests~~ → Done ✔️ - Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986 diff --git a/chart/files/default.yml b/chart/files/default.yml index e4689df792..d8774b6ea7 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -152,22 +152,6 @@ redis: # ID SETTINGS AFTER THAT! id: "aidx" - -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── diff --git a/compose.local-db.yml b/compose.local-db.yml deleted file mode 100644 index 9d200ea36d..0000000000 --- a/compose.local-db.yml +++ /dev/null @@ -1,75 +0,0 @@ -# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します - -services: - redis: - restart: always - image: redis:7-alpine - ports: - - "6379:6379" - volumes: - - ./redis:/data - healthcheck: - test: "redis-cli ping" - interval: 5s - retries: 20 - - db: - restart: always - image: postgres:15-alpine - ports: - - "5432:5432" - env_file: - - .config/docker.env - volumes: - - ./db:/var/lib/postgresql/data - healthcheck: - test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" - interval: 5s - retries: 20 - -# meilisearch: -# restart: always -# image: getmeili/meilisearch:v1.3.4 -# environment: -# - MEILI_NO_ANALYTICS=true -# - MEILI_ENV=production -# env_file: -# - .config/meilisearch.env -# volumes: -# - ./meili_data:/meili_data - -# opensearchとopensearch-dashboardsのdockerfileは実装時の最新版の辞書なので適宜書き換えてください -# opensearch: -# build: ./opensearch -# environment: -# - "server.ssl.enabled:false" -# - "discovery.type=single-node" -# - "OPENSEARCH_INITIAL_ADMIN_PASSWORD=opensearch-adminpassword" #強めのパスワードじゃないと怒られる -# - "plugins.security.disabled=true" -# ulimits: -# memlock: -# soft: -1 # Set memlock to unlimited (no soft or hard limit) -# hard: -1 -# nofile: -# soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536 -# hard: 65536 -# volumes: -# - ./os-data:/usr/share/opensearch/data -# networks: -# - internal_network -# - external_network - -#OpenSearchのダッシュボードを見る場合に必要 -# opensearch-dashboards: -# build: ./opensearch-dashboards -# ports: -# - 5601:5601 -# links: -# - opensearch -# expose: -# - '5601' -# environment: -# OPENSEARCH_HOSTS: 'http://opensearch:9200' -# networks: -# - internal_network -# - external_network diff --git a/compose_example.yml b/compose_example.yml deleted file mode 100644 index fd5b256ae5..0000000000 --- a/compose_example.yml +++ /dev/null @@ -1,134 +0,0 @@ -services: - web: - build: . - restart: always - links: - - db - - redis -# - mcaptcha -# - meilisearch - depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - ports: - - "3000:3000" - networks: - - internal_network - - external_network - # env_file: - # - .config/docker.env - volumes: - - ./files:/cherrypick/files - - ./.config:/cherrypick/.config:ro - - redis: - restart: always - image: redis:7-alpine - networks: - - internal_network - volumes: - - ./redis:/data - healthcheck: - test: "redis-cli ping" - interval: 5s - retries: 20 - - db: - restart: always - image: postgres:15-alpine - networks: - - internal_network - env_file: - - .config/docker.env - volumes: - - ./db:/var/lib/postgresql/data - healthcheck: - test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" - interval: 5s - retries: 20 - -# mcaptcha: -# restart: always -# image: mcaptcha/mcaptcha:latest -# networks: -# internal_network: -# external_network: -# aliases: -# - localhost -# ports: -# - 7493:7493 -# env_file: -# - .config/docker.env -# environment: -# PORT: 7493 -# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/" -# depends_on: -# db: -# condition: service_healthy -# mcaptcha_redis: -# condition: service_healthy -# -# mcaptcha_redis: -# image: mcaptcha/cache:latest -# networks: -# - internal_network -# healthcheck: -# test: "redis-cli ping" -# interval: 5s -# retries: 20 - -# opensearchとopensearch-dashboardsのdockerfileは実装時の最新版の辞書なので適宜書き換えてください -# opensearch: -# build: ./opensearch -# environment: -# - "server.ssl.enabled:false" -# - "discovery.type=single-node" -# - "OPENSEARCH_INITIAL_ADMIN_PASSWORD=opensearch-adminpassword" #強めのパスワードじゃないと怒られる -# - "plugins.security.disabled=true" -# ulimits: -# memlock: -# soft: -1 # Set memlock to unlimited (no soft or hard limit) -# hard: -1 -# nofile: -# soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536 -# hard: 65536 -# volumes: -# - ./os-data:/usr/share/opensearch/data -# networks: -# - internal_network -# - external_network - -#OpenSearchのダッシュボードを見る場合に必要 -# opensearch-dashboards: -# build: ./opensearch-dashboards -# ports: -# - 5601:5601 -# links: -# - opensearch -# expose: -# - '5601' -# environment: -# OPENSEARCH_HOSTS: 'http://opensearch:9200' -# networks: -# - internal_network -# - external_network - -# meilisearch: -# restart: always -# image: getmeili/meilisearch:v1.3.4 -# environment: -# - MEILI_NO_ANALYTICS=true -# - MEILI_ENV=production -# env_file: -# - .config/meilisearch.env -# networks: -# - internal_network -# volumes: -# - ./meili_data:/meili_data - -networks: - internal_network: - internal: true - external_network: diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.js similarity index 93% rename from cypress/e2e/basic.cy.ts rename to cypress/e2e/basic.cy.js index f0ea7a3d44..adcca0bd40 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - describe('Before setup instance', () => { beforeEach(() => { cy.resetState(); @@ -166,13 +161,11 @@ describe('After user signed in', () => { }); it('successfully loads', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible'); + cy.get('[data-cy-user-setup-continue]').should('be.visible'); }); it('account setup wizard', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click(); + cy.get('[data-cy-user-setup-continue]').click(); cy.get('[data-cy-user-setup-user-name] input').type('ありす'); cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); @@ -221,8 +214,7 @@ describe('After user setup', () => { cy.login('alice', 'alice1234'); // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); + cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click(); cy.get('[data-cy-modal-dialog-ok]').click(); }); diff --git a/cypress/e2e/router.cy.ts b/cypress/e2e/router.cy.ts deleted file mode 100644 index 8d8fb3af31..0000000000 --- a/cypress/e2e/router.cy.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -describe('Router transition', () => { - describe('Redirect', () => { - // サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い) - before(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - - cy.login('alice', 'alice1234'); - - // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); - cy.wait(500); - cy.get('[data-cy-modal-dialog-ok]').click(); - }); - - it('redirect to user profile', () => { - // テストのためだけに用意されたリダイレクト用ルートに飛ぶ - cy.visit('/redirect-test'); - - // プロフィールページのURLであることを確認する - cy.url().should('include', '/@alice') - }); - }); -}); diff --git a/cypress/e2e/widgets.cy.ts b/cypress/e2e/widgets.cy.js similarity index 95% rename from cypress/e2e/widgets.cy.ts rename to cypress/e2e/widgets.cy.js index 847801a69f..df6ec8357d 100644 --- a/cypress/e2e/widgets.cy.ts +++ b/cypress/e2e/widgets.cy.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /* flaky describe('After user signed in', () => { beforeEach(() => { diff --git a/cypress/support/commands.ts b/cypress/support/commands.js similarity index 87% rename from cypress/support/commands.ts rename to cypress/support/commands.js index 281f2e6ccd..91a4d7abe6 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.js @@ -30,13 +30,9 @@ Cypress.Commands.add('visitHome', () => { }) Cypress.Commands.add('resetState', () => { - // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 - // see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123 - /* - cy.window().then(win => { + cy.window(win => { win.indexedDB.deleteDatabase('keyval-store'); }); - */ cy.request('POST', '/api/reset-db', {}).as('reset'); cy.get('@reset').its('status').should('equal', 204); cy.reload(true); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.js similarity index 100% rename from cypress/support/e2e.ts rename to cypress/support/e2e.js diff --git a/cypress/support/index.ts b/cypress/support/index.ts deleted file mode 100644 index c1bed21979..0000000000 --- a/cypress/support/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare global { - namespace Cypress { - interface Chainable { - login(username: string, password: string): Chainable; - - registerUser( - username: string, - password: string, - isAdmin?: boolean - ): Chainable; - - resetState(): Chainable; - - visitHome(): Chainable; - } - } -} - -export {} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json deleted file mode 100644 index 6fe7f32cc4..0000000000 --- a/cypress/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "es5"], - "target": "es5", - "types": ["cypress", "node"] - }, - "include": ["./**/*.ts"] -} diff --git a/docker-compose.local-db.yml b/docker-compose.local-db.yml new file mode 100644 index 0000000000..16ba4b49e1 --- /dev/null +++ b/docker-compose.local-db.yml @@ -0,0 +1,42 @@ +version: "3" + +# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します + +services: + redis: + restart: always + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - ./redis:/data + healthcheck: + test: "redis-cli ping" + interval: 5s + retries: 20 + + db: + restart: always + image: postgres:15-alpine + ports: + - "5432:5432" + env_file: + - .config/docker.env + volumes: + - ./db:/var/lib/postgresql/data + healthcheck: + test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" + interval: 5s + retries: 20 + +# meilisearch: +# restart: always +# image: getmeili/meilisearch:v1.3.4 +# environment: +# - MEILI_NO_ANALYTICS=true +# - MEILI_ENV=production +# env_file: +# - .config/meilisearch.env +# volumes: +# - ./meili_data:/meili_data + diff --git a/docker-compose_example.yml b/docker-compose_example.yml new file mode 100644 index 0000000000..1dc184ce0c --- /dev/null +++ b/docker-compose_example.yml @@ -0,0 +1,67 @@ +version: "3" + +services: + web: + build: . + restart: always + links: + - db + - redis +# - meilisearch + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + ports: + - "3000:3000" + networks: + - internal_network + - external_network + volumes: + - ./files:/cherrypick/files + - ./.config:/cherrypick/.config:ro + + redis: + restart: always + image: redis:7-alpine + networks: + - internal_network + volumes: + - ./redis:/data + healthcheck: + test: "redis-cli ping" + interval: 5s + retries: 20 + + db: + restart: always + image: postgres:15-alpine + networks: + - internal_network + env_file: + - .config/docker.env + volumes: + - ./db:/var/lib/postgresql/data + healthcheck: + test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" + interval: 5s + retries: 20 + +# meilisearch: +# restart: always +# image: getmeili/meilisearch:v1.3.4 +# environment: +# - MEILI_NO_ANALYTICS=true +# - MEILI_ENV=production +# env_file: +# - .config/meilisearch.env +# networks: +# - internal_network +# volumes: +# - ./meili_data:/meili_data + +networks: + internal_network: + internal: true + external_network: diff --git a/docs/Advanced-Search.md b/docs/Advanced-Search.md deleted file mode 100644 index f9345cba01..0000000000 --- a/docs/Advanced-Search.md +++ /dev/null @@ -1,51 +0,0 @@ - -### 高度な検索の使い方 - -
OR検索 - -猫かにゃんを含むノートを検索 -``` -猫|にゃん -``` -|(パイプ)をキーワードの間に入れます -
- -
AND検索 - -猫とにゃんを含むノートを検索 -``` -猫 にゃん -``` -全角または半角のスペースをキーワードの間に入れます -
- -
NOT検索 - - -猫を含みにゃんを含まないノートを検索 -``` -猫 -にゃん -``` -半角スペースで区切ってから-(マイナス/ハイフン)キーワードの間に入れます -
- -
一致検索 -表記ゆれやあいまい検索がデフォルトで有効になっているので確実に指定したい場合 - -にゃんで検索するとにゃーんや、にゃーーんも検索で出てきます - -にゃんのみを含むノートを検索 -``` -"にゃん" -``` -"(ダブルコーテーション)でキーワードを囲います -
- - -
組み合わせ検索 - -にゃんを含み、猫または狐を含み、こやんを含まないノートを検索 -``` -"にゃん" (猫|狐) -こやん -``` -
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md deleted file mode 100644 index 45b748e820..0000000000 --- a/docs/DEVELOPMENT.md +++ /dev/null @@ -1,45 +0,0 @@ -# 開発ガイド -開発をする上で役にたつメモ - -## devcontainerの利用 -開発をする上でdevcontainerを利用すると、コンテナ上に開発環境を構築することができます。 - -> **Note** -> Windowsを利用している場合、リポジトリをローカルにクローンする必要があります。 - -実行する前に下記のコマンドを打つ必要があります。 -```bash -git clone https://github.com/1673beta/cherrypick.git -chmod 777 cherrypick -cp .devcontainer/devcontainer.yml .config/default.yml -``` -その後、VSCodeのコマンドパレットからコンテナをビルドして立ち上げ、下記のコマンドを実行することで開発サーバーが立ち上がります。 - -```bash -pnpm build -pnpm migrate -pnpm dev -``` - -## DBマイグレーションを作成する -Misskey/CherrypickではTypeORMを利用してDBマイグレーションを実行します。下記の手順で作成することができます。 - -```bash -cd packages/backend -pnpm dlx typeorm migration:generate -d ormconfig.js -o MigrationName -``` - -## フォーマット -Biomeを使ってコードフォーマットを統一することができます。 -下記ディレクトリで実行することができます。 -* packages/backend -* packages/frontend -* packages/cherrypick-js -* packages/sw - -下記コマンドを実行すると、フォーマットを修正することができます。 - -```bash -pnpm run format:write -``` - diff --git a/healthcheck.sh b/healthcheck.sh index 51f6274ed2..230e5a6560 100644 --- a/healthcheck.sh +++ b/healthcheck.sh @@ -1,7 +1,7 @@ #!/bin/bash -# SPDX-FileCopyrightText: syuilo and misskey-project +# SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors # SPDX-License-Identifier: AGPL-3.0-only PORT=$(grep '^port:' /cherrypick/.config/default.yml | awk 'NR==1{print $2; exit}') -curl -Sfso/dev/null "http://localhost:${PORT}/healthz" +curl -s -S -o /dev/null "http://localhost:${PORT}" diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index c6e2f46d72..84c6e99ca8 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -123,7 +123,6 @@ reactions: "التفاعلات" reactionSettingDescription2: "اسحب لترتيب ، انقر للحذف ، استخدم \"+\" للإضافة." rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات" attachCancel: "أزل المرفق" -deleteFile: "حُذف الملف" markAsSensitive: "علّمه كمحتوى حساس" unmarkAsSensitive: "ألغ تعيينه كمحتوى حساس" enterFileName: "ادخل اسم الملف" @@ -361,8 +360,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "فعّل hCaptcha" hcaptchaSiteKey: "مفتاح الموقع" hcaptchaSecretKey: "المفتاح السري" -mcaptchaSiteKey: "مفتاح الموقع" -mcaptchaSecretKey: "المفتاح السري" recaptcha: "reCAPTCHA" enableRecaptcha: "تمكين reCAPTCHA" recaptchaSiteKey: "مفتاح الموقع" @@ -1026,12 +1023,7 @@ expired: "منتهية صلاحيته" icon: "الصورة الرمزية" replies: "رد" renotes: "أعد النشر" -sourceCode: "الشفرة المصدرية" flip: "اقلب" -lastNDays: "آخر {n} أيام" -surrender: "ألغِ" -_delivery: - stop: "مُعلّق" _initialAccountSetting: accountCreated: "نجح إنشاء حسابك!" letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي." @@ -1278,6 +1270,8 @@ _sfx: notification: "الإشعارات" chat: "المحادثة" chatBg: "المحادثة (الخلفية)" + antenna: "الهوائيات" + channel: "إشعارات القنات" _ago: future: "المستقبَل" justNow: "اللحظة" @@ -1436,7 +1430,6 @@ _profile: _exportOrImport: allNotes: "كل الملاحظات" favoritedNotes: " الملاحظات المفضلة" - clips: "مِشبك" followingList: "المتابَعون" muteList: "المستخدمون المكتومون" blockingList: "المستخدمون المحجوبون" @@ -1582,27 +1575,7 @@ _webhookSettings: active: "مُفعّل" _events: reaction: "عند التفاعل" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "البريد الإلكتروني " _moderationLogTypes: suspend: "علِق" - deleteDriveFile: "حُذف الملف" - deleteNote: "حُذفت الملاحظة" - createGlobalAnnouncement: "أُنشئ إعلان عام" - createUserAnnouncement: "أُنشئ إعلان مستخدم" - updateGlobalAnnouncement: "حُدث إعلان عام" - updateUserAnnouncement: "حُدث إعلان مستخدم" resetPassword: "أعد تعيين كلمتك السرية" createInvitation: "ولِّد دعوة" -_reversi: - total: "المجموع" - lookingForPlayer: "يبحث عن خصم..." - gameCanceled: "أُلغيت اللعبة." - opponentHasSettingsChanged: "غيَر الخصم إعدادته." - showBoardLabels: "اعرض ترقيم الصفوف والأعمدة على اللوح" - useAvatarAsStone: "حوَل الحجارة إلى صور مستخدمين" -_offlineScreen: - title: "غير متصل - يتعذر الاتصال بالخادم" - header: "يتعذر الاتصال بالخادم" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index ec3285f5c2..85fa28a472 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -357,8 +357,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptcha চালু করুন" hcaptchaSiteKey: "সাইট কী" hcaptchaSecretKey: "সিক্রেট কী" -mcaptchaSiteKey: "সাইট কী" -mcaptchaSecretKey: "সিক্রেট কী" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA চালু করুন" recaptchaSiteKey: "সাইট কী" @@ -868,12 +866,7 @@ youFollowing: "অনুসরণ করা হচ্ছে" icon: "প্রোফাইল ছবি" replies: "জবাব" renotes: "রিনোট" -sourceCode: "সোর্স কোড" flip: "উল্টান" -_delivery: - stop: "স্থগিত করা হয়েছে" - _type: - none: "প্রকাশ করা হচ্ছে" _role: priority: "অগ্রাধিকার" _priority: @@ -1112,6 +1105,8 @@ _sfx: notification: "বিজ্ঞপ্তি" chat: "চ্যাট" chatBg: "চ্যাট (ব্যাকগ্রাউন্ড)" + antenna: "অ্যান্টেনাগুলি" + channel: "চ্যানেলের বিজ্ঞপ্তি" _ago: future: "ভবিষ্যৎ" justNow: "এইমাত্র" @@ -1128,6 +1123,29 @@ _time: minute: "মিনিট" hour: "ঘণ্টা" day: "দিন" +_tutorial: + title: "CherryPick কিভাবে ব্যাবহার করবেন" + step1_1: "স্বাগতম!" + step1_2: "এই স্ক্রীনটিকে \"টাইমলাইন\" বলা হয় এবং কালানুক্রমিক ক্রমে আপনার এবং আপনি যাদের \"অনুসরণ করেন\" তাদের \"নোটগুলি\" দেখায়৷" + step1_3: "আপনি আপনার টাইমলাইনে কিছু দেখতে পাবেন না কারণ আপনি এখনও কোনো নোট পোস্ট করেননি এবং আপনি কাউকে অনুসরণ করছেন না৷" + step2_1: "নোট তৈরি করার আগে বা কাউকে অনুসরণ করার আগে প্রথমে আপনার প্রোফাইলটি সম্পূর্ণ করুন।" + step2_2: "আপনি কে তা জানা অনেক লোকের জন্য আপনার নোটগুলি দেখা এবং অনুসরণ করাকে সহজ করে তোলে৷" + step3_1: "আপনি কি সফলভাবে আপনার প্রোফাইল সেট আপ করেছেন?" + step3_2: "এখন, কিছু নোট পোস্ট করার চেষ্টা করুন। পোস্ট ফর্ম খুলতে পেন্সিল চিহ্নযুক্ত বাটনে ক্লিক করুন।" + step3_3: "বিষয়বস্তু লেখার পরে, আপনি ফর্মের উপরের ডানদিকের বাটনে ক্লিক করে পোস্ট করতে পারেন।" + step3_4: "পোস্ট করার মত কিছু মনে পরছে না? \"আমি মিসকি সেট আপ করছি\" বললে কেমন হয়?" + step4_1: "পোস্ট করেছেন?" + step4_2: "সাবাশ! এখন আপনার নোট টাইমলাইনে দেখা যাবে।" + step5_1: "এখন অন্যদেরকে অনুসরণ করে আপনার টাইমলাইনকে প্রাণবন্ত করে তুলুন।" + step5_2: "আপনি {featured}-এ জনপ্রিয় নোটগুলি দেখতে পারেন, যাতে আপনি যে ব্যক্তিকে পছন্দ করেন তাকে বেছে নিতে এবং অনুসরণ করতে পারেন, অথবা {explore}-এ জনপ্রিয় ব্যবহারকারীদের দেখতে পারেন৷" + step5_3: "একজন ব্যবহারকারীকে অনুসরণ করতে, ব্যবহারকারীর আইকনে ক্লিক করুন এবং ব্যবহারকারীর পৃষ্ঠাতে \"অনুসরণ করুন\" বাটনে ক্লিক করুন।" + step5_4: "যদি ব্যবহারকারীর নামের পাশে একটি লক আইকন থাকে তাহলে আপনার অনুসরণের অনুরোধ গ্রহণ করার জন্য তারা কিছু সময় নিতে পারে।" + step6_1: "সবকিছু ঠিক থাকলে আপনি টাইমলাইনে অন্য ব্যবহারকারীদের নোট দেখতে পাবেন।" + step6_2: "আপনি সহজেই আপনার প্রতিক্রিয়া জানাতে অন্য ব্যক্তির নোটে \"রিঅ্যাকশন\" যোগ করতে পারেন।" + step6_3: "একটি রিঅ্যাকশন যোগ করতে, নোটে \"+\" চিহ্নে ক্লিক করুন এবং আপনার পছন্দের রিঅ্যাকশন নির্বাচন করুন।" + step7_1: "অভিনন্দন! আপনি এখন CherryPick-র প্রাথমিক টিউটোরিয়ালটি শেষ করেছেন।" + step7_2: "আপনি যদি CherryPick সম্পর্কে আরও জানতে চান, তাহলে {help} এ দেখুন।" + step7_3: "এখন CherryPick উপভোগ করুন 🚀" _2fa: alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷" step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷" @@ -1275,7 +1293,6 @@ _profile: changeBanner: "ব্যানার পরিবর্তন করুন" _exportOrImport: allNotes: "সকল নোট" - clips: "ক্লিপ" followingList: "অনুসরণ করা হচ্ছে" muteList: "মিউট" blockingList: "ব্লক" @@ -1426,12 +1443,6 @@ _deck: _webhookSettings: name: "নাম" active: "চালু" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "ইমেইল" _moderationLogTypes: suspend: "স্থগিত করা" resetPassword: "পাসওয়ার্ড রিসেট করুন" -_reversi: - total: "মোট" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 17af74ba4d..a06d1f2442 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -130,7 +130,6 @@ overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats" reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir." rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes" attachCancel: "Eliminar el fitxer adjunt" -deleteFile: "Esborrar l'arxiu " markAsSensitive: "Marcar com a NSFW" unmarkAsSensitive: "Deixar de marcar com a sensible" enterFileName: "Defineix nom del fitxer" @@ -337,12 +336,8 @@ whenServerDisconnected: "Quan es perdi la connexió al servidor" disconnectedFromServer: "Desconnectat pel servidor" reload: "Actualitza" doNothing: "Ignora" -reloadConfirm: "Vols recarregar?" -watch: "Veure" -unwatch: "Deixar de veure" -accept: "Acceptar" -reject: "Denegar" -normal: "Normal" +accept: "Accepta" +normal: "Nomal" instanceName: "Nom del servidor" instanceDescription: "Descripció del servidor" maintainerName: "Nom de l'administrador" @@ -360,57 +355,25 @@ connectService: "Connecta" disconnectService: "Desconnecta" enableLocalTimeline: "Activa la línia de temps local" enableGlobalTimeline: "Activa la línia de temps global" -disablingTimelinesInfo: "Fins i tot si aquestes línies de temps són desactivades, els administradors i els moderadors poden continuar visualitzant per conveniència." registration: "Registre" -enableRegistration: "Permet els registres d'usuaris" invite: "Convida" -driveCapacityPerLocalAccount: "Capacitat del disc per usuaris locals" -driveCapacityPerRemoteAccount: "Capacitat del disc per usuaris remots" -inMb: "En megabytes" -bannerUrl: "Adreça URL del bàner" -backgroundImageUrl: "Adreça URL de la imatge de fons" basicInfo: "Informació bàsica" pinnedUsers: "Usuaris fixats" -pinnedUsersDescription: "Llista d'usuaris, separats per salts de línia, que seran fixats a la pestanya \"Explorar\"." -pinnedPages: "Pàgines fixades" -pinnedPagesDescription: "Escriu els camins de les pàgines que vols fixar a la pàgina d'inici d'aquesta instància. Separades per salts de línia." -pinnedClipId: "ID del retall fixat" pinnedNotes: "Nota fixada" -hcaptcha: "hCaptcha" -enableHcaptcha: "Activar hCaptcha" -hcaptchaSiteKey: "Clau del lloc" -hcaptchaSecretKey: "Clau secreta" -mcaptcha: "mCaptcha" -enableMcaptcha: "Activar mCaptcha" -mcaptchaSiteKey: "Clau del lloc" -mcaptchaSecretKey: "Clau secreta" -mcaptchaInstanceUrl: "Adreça URL del servidor mCaptcha" -recaptcha: "reCAPTCHA" -enableRecaptcha: "Activar reCAPTCHA" -recaptchaSiteKey: "Clau del lloc" -recaptchaSecretKey: "Clau secreta" turnstile: "Turnstile" enableTurnstile: "Activar Turnstile" turnstileSiteKey: "Clau del lloc" turnstileSecretKey: "Clau secreta" -avoidMultiCaptchaConfirm: "Fer servir diferents sistemes de Captcha a la vegada pot causar problemes entre ells. Vols desactivar els altres sistemes de Captcha activats? Si els vols mantenir actius fes clic a cancel·lar." antennas: "Antena" manageAntennas: "Gestiona les antenes" -name: "Nom" antennaSource: "Font de l'antena" antennaKeywords: "Paraules clau a seguir" antennaExcludeKeywords: "Paraules clau a excloure" -antennaExcludeBots: "Exclou els bots" antennaKeywordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR." notifyAntenna: "Notifica'm les publicacions noves" withFileAntenna: "Només les publicacions amb fitxers" -enableServiceworker: "Activar les notificacions al navegador" antennaUsersDescription: "Llistar un nom d'usuari per línia" -caseSensitive: "Sensible a majúscules i minúscules " -withReplies: "Inclou respostes" -connectedTo: "Aquests comptes hi són connectats" notesAndReplies: "Amb respostes" -withFiles: "Incloure arxius" silence: "Silencia" silenceConfirm: "Segur que vols silenciar aquest usuari?" unsilence: "Deixa de silenciar" @@ -426,40 +389,20 @@ userList: "Llistes" about: "Informació" aboutMisskey: "Quant a CherryPick" administrator: "Administrador/a" -token: "Codi de verificació" -2fa: "Autenticació de doble factor" -setupOf2fa: "Configurar l'autenticació de doble factor" -totp: "Aplicació d'autenticació" -totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'autenticació" moderator: "Moderador/a" moderation: "Moderació" -moderationNote: "Nota de moderació " -addModerationNote: "Afegir una nota de moderació " -moderationLogs: "Registre de moderació " nUsersMentioned: "{n} usuaris mencionats" -securityKeyAndPasskey: "Clau de seguretat / Clau de pas" securityKey: "Clau de seguretat" -lastUsed: "Fet servir per última vegada" -lastUsedAt: "Fet servir per última vegada: {t}" unregister: "Cancel·la el registre" passwordLessLogin: "Inici de sessió sense contrasenya" -passwordLessLoginDescription: "Permet l'inici de sessió sense contrasenya fent servir només una Clau de seguretat/Clau de pas" resetPassword: "Restableix la contrasenya" newPasswordIs: "La contrasenya nova és «{password}»" reduceUiAnimation: "Redueix les animacions de la interfície" share: "Comparteix" notFound: "No s'ha trobat" -notFoundDescription: "No es troba cap pàgina que correspongui a aquesta adreça" -uploadFolder: "Carpeta per defecte per pujades" -markAsReadAllNotifications: "Marca totes les notificacions com a llegides" markAsReadAllUnreadNotes: "Marca-ho tot com a llegit" -markAsReadAllTalkMessages: "Marcar tots els missatges com llegits" help: "Ajuda" -inputMessageHere: "Escriu aquí el teu missatge " -close: "Tancar" invites: "Convida" -members: "Membres" -transfer: "Transferir" title: "Títol" text: "Text" enable: "Habilita" @@ -495,7 +438,6 @@ emojiStyle: "Estil d'emoji" native: "Nadiu" disableDrawer: "No mostrar els menús en calaixos" showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor" -showReactionsCount: "Mostra el nombre de reaccions a les publicacions" noHistory: "No hi ha un registre previ" signinHistory: "Historial d'autenticacions" enableAdvancedMfm: "Habilitar l'MFM avançat" @@ -530,62 +472,12 @@ objectStorage: "Emmagatzematge d'objectes\n" useObjectStorage: "Utilitzar l'emmagatzematge d'objectes" objectStorageBaseUrl: "Base d'enllaç" objectStorageBaseUrlDesc: "Prefix d'enllaç utilitzat per a fer referencia als fitxers. Especifica l'enllaç del teu CDN o Proxy si n'estàs utilitzant qualsevol, en cas contrari, especifica l'enllaç al que es pot accedir públicament segons la guia de servei que vosté utilitza.\nPer l'ús d'S3 utilitza 'https://.s3.amazonaws.com' I per a GCS o serveis equivalents utilitza 'https://storage.googleapis.com/'." -objectStorageBucket: "Dipòsit " -objectStorageBucketDesc: "Escriu el nom del dipòsit que fas servir al teu proveïdor d'emmagatzematge " -objectStoragePrefix: "Prefix" -objectStoragePrefixDesc: "Els fitxers es deixaren a directoris amb aquest prefix" -objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "Deixa'l buit si fas servir AWS S3, si no és així específica un punt d'entrada com '' o ':', depenent del servei que facis servir." -objectStorageRegion: "Regió " -objectStorageRegionDesc: "Especifica una regió com 'xx-east-1'. Si el teu servei no diferència regions has de posar 'us-east-1'. Deixa'l buit si fas servir variables d'entorn o un arxiu de configuració d'AWS." -objectStorageUseSSL: "Fes servir SSL" -objectStorageUseSSLDesc: "Desactiva'l si no tens pensat fer servir HTTPS per les connexions de l'API" -objectStorageUseProxy: "Connectar-se mitjançant un Proxy" -objectStorageUseProxyDesc: "Desactiva'l si no faràs servir un Proxy per les connexions de l'API" -objectStorageSetPublicRead: "Configurar les pujades com públiques " -s3ForcePathStyleDesc: "Si s3ForcePathStyle es troba activat el nom del dipòsit s'ha d'incloure a l'adreça URL en comtes del nom del host. Potser que necessitis activar-ho quan facis servir, per exemple, Minio a un servidor propi." -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." newNoteRecived: "Hi ha publicacions noves" -sounds: "Sons" -sound: "So" -listen: "Escoltar" -none: "Res" -showInPage: "Mostrar a la pàgina " -popout: "Finestra emergent" -volume: "Volum" -masterVolume: "Volum principal" -notUseSound: "Sense so" -useSoundOnlyWhenActive: "Reproduir sons només quan CherryPick estigui actiu" -details: "Detalls" -chooseEmoji: "Tria un emoji" -unableToProcess: "L'operació no pot ser completada " -recentUsed: "Utilitzat recentment" -install: "Instal·lació " -uninstall: "Desinstal·lar " -installedApps: "Aplicacions autoritzades " -nothing: "No hi ha res per veure aquí " installedDate: "Data d'instal·lació" -lastUsedDate: "Utilitzat per última vegada" state: "Estat" sort: "Ordena" ascendingOrder: "Ascendent" descendingOrder: "Descendent" -scratchpad: "Bloc de proves" -scratchpadDescription: "El bloc de proves proporciona un entorn experimental per AiScript. Pot escriure i verificar els resultats que interactuen amb CherryPick." -output: "Sortida" -script: "Script" -disablePagesScript: "Desactivar AiScript a les pàgines " -updateRemoteUser: "Actualitzar la informació de l'usuari remot" -unsetUserAvatar: "Desactivar l'avatar " -unsetUserAvatarConfirm: "Segur que vols desactivar l'avatar?" -unsetUserBanner: "Desactivar el bàner " -unsetUserBannerConfirm: "Segur que vols desactivar el bàner?" -deleteAllFiles: "Esborrar tots els arxius" -deleteAllFilesConfirm: "Segur que vols esborrar tots els arxius?" removeAllFollowing: "Deixar de seguir tots els usuaris seguits" 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" @@ -634,1399 +526,49 @@ medium: "Mitjà" small: "Petit" generateAccessToken: "Genera codi d'accés" permission: "Permisos" -adminPermission: "Permisos d'administrador " enableAll: "Habilita tot" disableAll: "Deshabilita tot" tokenRequested: "Donar accés al compte" -pluginTokenRequestedDescription: "Aquest connector podrà fer servir tots els permisos configurats aquí." -notificationType: "Tipus de notificació " -edit: "Editar" -emailServer: "Servidor de correu electrònic " -enableEmail: "Activar l'enviament de correus electrònics " -emailConfigInfo: "Es fa servir per confirmar el teu correu quan et registres o oblides la contrasenya " -email: "Correu electrònic" -emailAddress: "Adreça de correu electrònic" -smtpConfig: "Configuració del servidor SMTP" smtpHost: "Amfitrió" -smtpPort: "Port" smtpUser: "Nom d'usuari" smtpPass: "Contrasenya" -emptyToDisableSmtpAuth: "No omplis el nom d'usuari i la contrasenya si vols deshabilitar l'autenticació SMTP" -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 " -hardWordMute: "Silenciar paraules fortes" -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" -makeActive: "Activar" -display: "Veure" -copy: "Copiar" -metrics: "Mètriques" -overview: "Visió General" -logs: "Registres" -delayed: "Endarrerits " -database: "Bases de dades" -channel: "Canals" -create: "Crear" -notificationSetting: "Paràmetres de notificacions" -notificationSettingDesc: "Selecciona els tipus de notificacions que es mostraran" -useGlobalSetting: "Fer servir la configuració global" -useGlobalSettingDesc: "Si s'activa, es farà servir la configuració de notificacions del teu comte. Si no s'activa es poden fer configuracions individuals." -other: "Altre" -regenerateLoginToken: "Regenerar clau de seguretat d'inici de sessió" -regenerateLoginTokenDescription: "Regenera la clau de seguretat que es fa servir internament durant l'inici de sessió. Normalment aquesta acció no és necessària. Si es regenera es tancarà la sessió a tots els dispositius amb una sessió activa." -theKeywordWhenSearchingForCustomEmoji: "Cercar un emoji personalitzat " -setMultipleBySeparatingWithSpace: "Separa múltiples entrades amb un espai" -fileIdOrUrl: "ID de l'arxiu o URL" -behavior: "Comportament" -sample: "Mostrar" -abuseReports: "Denúncies " -reportAbuse: "Denuncia un abús " -reportAbuseRenote: "Denuncia una renota" -reportAbuseOf: "Denuncia a {name}" -fillAbuseReportDescription: "Omple els detalls sobre aquesta denúncia. Si la denúncia és sobre una nota en concret inclou l'adreça URL." -abuseReported: "La teva denúncia s'ha enviat. Moltes gràcies." -reporter: "Denunciant " -reporteeOrigin: "Origen de la denúncia " -reporterOrigin: "Origen del denunciant" -forwardReport: "Transferir la denúncia a una instància remota" -forwardReportIsAnonymous: "En lloc del teu compte, es farà servir un compte anònim com a denunciant al servidor remot." -send: "Envia" -abuseMarkAsResolved: "Marca la denúncia com a resolta" -openInNewTab: "Obre a una pestanya nova" -openInSideView: "Obre a una vista lateral" -defaultNavigationBehaviour: "Navegació per defecte" -editTheseSettingsMayBreakAccount: "Editar aquestes opcions pot deixar inoperatiu el teu compte" -instanceTicker: "Informació de notes de la instància " -waitingFor: "Esperant {x}" -random: "Aleatori " -system: "Sistema" -switchUi: "Canviar interfície d'usuari " -desktop: "Escriptori" -clip: "Retalls" -createNew: "Crear" -optional: "Opcional" -createNewClip: "Crear un nou Retall" -unclip: "Treure Retall" -confirmToUnclipAlreadyClippedNote: "Aquesta nota ja és inclosa al Retall \"{name}\". Vols treure-la d'aquest retall?" -public: "Públic " -private: "Privat" -i18nInfo: "CherryPick està sent traduït a diferents idiomes per voluntaris. Pots ajudar aquí {link}." -manageAccessTokens: "Administrar claus de seguretat d'accés " -accountInfo: "Informació del compte" -notesCount: "Comptador de notes" -repliesCount: "Nombre de respostes" renotesCount: "Impulsos fets" -repliedCount: "Nombre de respostes rebudes" renotedCount: "Impulsos rebuts" -followingCount: "Nombre de comptes seguits" -followersCount: "Nombre de seguidors" -sentReactionsCount: "Nombre de reaccions enviades" -receivedReactionsCount: "Nombre de reaccions rebudes" -pollVotesCount: "Nombre de vots enviats a enquestes" -pollVotedCount: "Nombre de vots rebuts a les enquestes" -yes: "Sí " -no: "No" -driveFilesCount: "Nombre de fitxers al Disc" -driveUsage: "Utilització de l'espai del Disc" -noCrawle: "Rebutjar la indexació dels buscadors" -noCrawleDescription: "No permetis que els buscadors indexin el teu perfil, notes, pàgines, etc." -lockedAccountInfo: "Tret que establiu la visibilitat de la nota a \"Només seguidors\", les vostres notes seran visibles per qualsevol persona, fins i tot si heu d'aprovar els seguidors manualment" -alwaysMarkSensitive: "Marcar com a sensible per defecte" -loadRawImages: "Carregar les imatges originals en comptes de miniatures " -disableShowingAnimatedImages: "No reproduir imatges animades" -highlightSensitiveMedia: "Ressalta els medis marcats com a sensibles" -verificationEmailSent: "S'ha enviat un correu electrònic de verificació. Fes clic a l'enllaç per completar la verificació." -notSet: "Sense definir" -emailVerified: "El correu electrònic s'ha verificat" -noteFavoritesCount: "Nombre de notes favorites " -pageLikesCount: "Nombre de Pàgines que t'agraden " -pageLikedCount: "Nombre d'agraïments rebuts a les Pàgines " -contact: "Contacte" -useSystemFont: "Fes servir la font per defecte del sistema" -clips: "Retalls" -experimentalFeatures: "Característiques experimentals" -experimental: "Experimental" -thisIsExperimentalFeature: "Aquesta és una característica experimental. La seva funcionalitat pot canviar, i pot ser que no funcioni degudament." -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" -duplicate: "Duplicat" -left: "Esquerra" -center: "Centre" -wide: "Gran" -narrow: "Estret" -reloadToApplySetting: "Aquest ajust només s'aplicarà després de recarregar la pàgina. Vols fer-ho ara?" -needReloadToApply: "Es requereix recarregar per reflectir aquesta opció " -showTitlebar: "Mostra la barra del títol " clearCache: "Esborra la memòria cau" -onlineUsersCount: "{n} Usuaris es troben en línia " -nUsers: "{n} Usuaris" -nNotes: "{n} Notes" -sendErrorReports: "Enviar informes d'error " -sendErrorReportsDescription: "Quan s'activa, es compartirà amb CherryPick informació detallada de l'error quan es trobi un problema això farà pujar la qualitat de CherryPick.\nAixò inclourà informació com la versió del SO que fas servir, el navegador web que fas servir, la teva activitat a CherryPick, etc." -myTheme: "El meu tema" -backgroundColor: "Color de fons" -accentColor: "Color principal" -textColor: "Color del text" -saveAs: "Desar com..." -advanced: "Avançat" -advancedSettings: "Configuració avançada" -value: "Valor" -createdAt: "Creat el" -updatedAt: "Actualitzat el" -saveConfirm: "Desar canvis?" -deleteConfirm: "Segur que vols esborrar?" -invalidValue: "Valor invàlid." -registry: "Registre " -closeAccount: "Tancar el compte" -currentVersion: "Versió actual" -latestVersion: "Versió nova" -youAreRunningUpToDateClient: "Ja estàs fent servir la versió més recent del client." -newVersionOfClientAvailable: "Tens disponible una versió del client més recent." -usageAmount: "Ús " -capacity: "Capacitat" -inUse: "Fet servir" -editCode: "Editar el codi" -apply: "Aplicar" -receiveAnnouncementFromInstance: "Rep notificacions d'aquesta instància " -emailNotification: "Notificacions per correu electrònic " -publish: "Publicar" -inChannelSearch: "Cerca al canal" -useReactionPickerForContextMenu: "Fes clic al botó dret del ratolí per obrir el menú de reaccions" -typingUsers: "{users} està/estàn Escrivint " -jumpToSpecifiedDate: "Ves a una data concreta" showingPastTimeline: "Estàs veient una línia de temps antiga" -clear: "Tornar" -markAllAsRead: "Marcar tot com llegit" -goBack: "Tornar" -unlikeConfirm: "Vols esborrar el teu m'agrada?" -fullView: "Vista completa." -quitFullView: "Sortir de la vista completa" -addDescription: "Afegeix una descripció " -userPagePinTip: "Podeu seleccionar \"Fixar al perfil\" del menú de notes individuals per mostrar les notes aquí." -notSpecifiedMentionWarning: "Aquesta nota esmenta usuaris que no es troben com a destinataris" info: "Informació" -userInfo: "Informació de l'usuari" -unknown: "Desconegut" -onlineStatus: "Connectat" -hideOnlineStatus: "Ocultar l'estat de connexió" -hideOnlineStatusDescription: "Ocultant el teu estat de connexió redueix les funcionalitats d'algunes funcions com la cerca." -online: "Connectat" -active: "Actiu" -offline: "Desconnectat" -notRecommended: "No recomanat" -botProtection: "Protecció contra bots" -instanceBlocking: "Instàncies blocades/silenciades" -selectAccount: "Seleccionar un compte" -switchAccount: "Canviar de compte" -enabled: "Activat" -disabled: "Desactivat" -quickAction: "Accions ràpides" user: "Usuaris" administration: "Administració" -accounts: "Comptes" -switch: "Canvia" -noMaintainerInformationWarning: "La informació de l'administrador no s'ha configurat" -noBotProtectionWarning: "La protecció contra bots no s'ha configurat." -configure: "Configurar" -postToGallery: "Crear una nova publicació a la galeria" -postToHashtag: "Pública a aquesta etiqueta" -gallery: "Galeria" -recentPosts: "Articles recents" -popularPosts: "Articles populars" -shareWithNote: "Comparteix amb una nota" -ads: "Anuncis" -expiration: "" -startingperiod: "Inici" -memo: "Recordatori" -priority: "Prioritat" -high: "Alta" middle: "Mitjà" -low: "Baixa" -emailNotConfiguredWarning: "Adreça de correu electrònic" -ratio: "Proporció" -previewNoteText: "Mostrar vista prèvia" -customCss: "CSS personalitzat" -customCssWarn: "Aquesta configuració només hauries de configurar-la si saps que fas. Si poses valors inadequats pots fer que el client deixi de funcionar correctament." global: "Global" -squareAvatars: "Mostrar avatars quadrats" -sent: "Envia" -received: "Rebut" -searchResult: "Resultats de la cerca" -hashtags: "Etiquetes" -troubleshooting: "Solucionar problemes" -useBlurEffect: "Fes servir efectes de desenfocament a la interfície" -learnMore: "Saber més " -misskeyUpdated: "CherryPick s'ha actualitzat " -whatIsNew: "Mostra canvis" -translate: "Traduir " -translatedFrom: "Traduït del {x}" -accountDeletionInProgress: "S'està produint l'eliminació del compte" -usernameInfo: "Un nom que identifiqui el teu compte d'altres en aquest servidor. Pots fer servir lletres (a~z, A~Z), números (0~9) i guions baixos (_). Els noms d'usuari no es poden canviar després." -aiChanMode: "Mode IA" -devMode: "Mode desenvolupador" -keepCw: "Mantenir els avisos de contingut" -pubSub: "Comptes Pub/Sub" -lastCommunication: "Última comunicació " -resolved: "Resolt" -unresolved: "Sense resoldre" -breakFollow: "Deixar de seguir" -breakFollowConfirm: "Vols deixar de seguir?" -itsOn: "Activat" -itsOff: "Desactivat" -on: "Activar" -off: "Desactivar" -emailRequiredForSignup: "Demanar correu electrònic per registrar-se " -unread: "Sense llegir" -filter: "Filtrar" -controlPanel: "Panel de control" -manageAccounts: "Gestionar comptes" -makeReactionsPublic: "Reaccions públiques " -makeReactionsPublicDescription: "Això fa que totes les teves reaccions siguin visibles públicament " -classic: "Clàssic " -muteThread: "Silenciar el fil" -unmuteThread: "Deixar de silenciar el fil" -followingVisibility: "Visibilitat dels seguiments" -followersVisibility: "Visibilitat dels seguidors" -continueThread: "Veure la continuació del fil" -deleteAccountConfirm: "Això eliminarà el teu compte irreversiblement. Procedir?" -incorrectPassword: "Contrasenya incorrecta." -voteConfirm: "Confirma el teu vot \"{choice}\"" -hide: "Amagar" -useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calaix al mòbil " -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" -tablet: "Tauleta" -auto: "Automàtic " -themeColor: "Color del tema" -size: "Mida" -numberOfColumn: "Nombre de columnes" searchByGoogle: "Cercar" -instanceDefaultLightTheme: "Tema clar per defecte de tota la instància " -instanceDefaultDarkTheme: "Tema fosc per defecte de tota la instància " -instanceDefaultThemeDescription: "Introdueix el codi del tema en format d'objecte" -mutePeriod: "Duració del silenci" -period: "Límit de temps" -indefinitely: "Permanent" -tenMinutes: "10 minuts" -oneHour: "1 hora" -oneDay: "Un dia" -oneWeek: "Una setmana" -oneMonth: "Un mes" -reflectMayTakeTime: "Això pot trigar una estona a tenir efecte" -failedToFetchAccountInformation: "No es pot obtenir la informació del compte" -rateLimitExceeded: "S'ha arribat al màxim de peticions" -cropImage: "Retalla la imatge" -cropImageAsk: "Vols retallar la imatge?" -cropYes: "Retallar" -cropNo: "Fer servir tal qual" file: "Fitxers" -recentNHours: "Últimes {n} hores" -recentNDays: "Últims {n} dies" -noEmailServerWarning: "Correu electrònic del servidor sense configurar" -thereIsUnresolvedAbuseReportWarning: "Hi ha informes sense solucionar." -recommended: "Recomanat" -check: "Verificar" -driveCapOverrideLabel: "Canvia la capacitat del Disc per aquest usuari" -driveCapOverrideCaption: "Restableix la mida original posant un valor de 0 o menys." -requireAdminForView: "Has de ser administrador per poder veure això." -isSystemAccount: "Un compte creat i operat automàticament pel sistema." -typeToConfirm: "Si us plau, escriu {x} per confirmar" -deleteAccount: "Esborrar el compte" -document: "Documentació" -numberOfPageCache: "Nombre de pàgines a la memòria cau" -numberOfPageCacheDescription: "Incrementant aquest nombre farà que millori l'experiència de l'usuari, però es farà servir més memòria al dispositiu de l'usuari." -logoutConfirm: "Vols sortir?" -lastActiveDate: "Fet servir per última vegada" -statusbar: "Barra d'estat" -pleaseSelect: "Selecciona una opció" -reverse: "Invertir" -colored: "Colorit" -refreshInterval: "Interval d'actualització " -label: "Etiqueta" -type: "Tipus" -speed: "Velocitat" -slow: "Lent" -fast: "Ràpid " -sensitiveMediaDetection: "Detecció de contingut sensible" -localOnly: "Només local" -remoteOnly: "Només remot" -failedToUpload: "Ha fallat la pujada" -cannotUploadBecauseInappropriate: "Aquest fitxer no es pot pujar perquè s'ha trobat que algunes parts són inapropiades." -cannotUploadBecauseNoFreeSpace: "Ha fallat la pujada del fitxer perquè no hi ha capacitat al Disc." -cannotUploadBecauseExceedsFileSizeLimit: "Aquest fitxer no es pot pujar perquè supera la mida permesa." -beta: "Proves" -enableAutoSensitive: "Marcar com a sensible automàticament " -enableAutoSensitiveDescription: "Permet la detecció i el marcat automàtic dels mitjans sensibles fent servir aprenentatge automàtic quan sigui possible. Si aquesta opció es troba desactivada potser que estigui activada per a tota la instància. " -activeEmailValidationDescription: "Activa la validació estricta de comptes de correu electrònic, inclou la validació d'adreces d'un sol ús i si es possible comunicar-se amb aquestes. Quan es troba desactivada només es vàlida el format del correu electrònic." -navbar: "Barra de navegació " -shuffle: "Aleatori" -account: "Compte" -move: "Mou" -pushNotification: "Enviament de notificacions" -subscribePushNotification: "Activar l'enviament de notificacions" -unsubscribePushNotification: "Desactivar l'enviament de notificacions" -pushNotificationAlreadySubscribed: "L'enviament de notificacions ja és activat" -pushNotificationNotSupported: "El teu navegador o la teva instància no suporta l'enviament de notificacions " -sendPushNotificationReadMessage: "Esborrar les notificacions enviades quan s'hagin llegit" -sendPushNotificationReadMessageCaption: "Això pot fer que el teu dispositiu consumeixi més bateria" -windowMaximize: "Maximitzar " -windowMinimize: "Minimitzar" -windowRestore: "Restaurar" -caption: "Llegenda" -loggedInAsBot: "Identificat com a bot" -tools: "Eines" -cannotLoad: "No es pot carregar" -numberOfProfileView: "Visualitzacions del perfil" -like: "M'agrada " -unlike: "Treure m'agrada " -numberOfLikes: "M'agraden " -show: "Veure" -neverShow: "No mostrar més " -remindMeLater: "Recorda-m'ho més tard" -didYouLikeMisskey: "T'està agradant CherryPick?" -pleaseDonate: "A {host} fem servir el software lliure CherryPick. Considera fer un donatiu a CherryPick perquè pugui continuar el seu desenvolupament!" -correspondingSourceIsAvailable: "El codi font corresponent està disponible a {anchor}." -roles: "Rols" -role: "Rols" -noRole: "No s'han trobat rols" -normalUser: "Usuari normal" -undefined: "Sense definir" -assign: "Assignar " -unassign: "Treure" -color: "Color" -manageCustomEmojis: "Gestiona els emojis personalitzats" -manageAvatarDecorations: "Gestiona les decoracions dels avatars " -youCannotCreateAnymore: "Has arribat al màxim de creacions" -cannotPerformTemporary: "Temporalment no disponible" -cannotPerformTemporaryDescription: "Aquesta acció no es pot dur a terme temporalment per arribar al seu límit d'execució. Pots esperar una mica i tornar-ho a intentar." -invalidParamError: "Paràmetres incorrectes " -invalidParamErrorDescription: "Els paràmetres demanats no són correctes. Normalment això es deu a un error, però també pot ser a alguna entrada excedint els límits o similar." -permissionDeniedError: "Operació no permesa " -permissionDeniedErrorDescription: "Aquest compte no té suficients permisos per dur a terme aquesta acció " -preset: "Predefinit" -selectFromPresets: "Escull des dels predefinits" -achievements: "Assoliments" -gotInvalidResponseError: "Resposta del servidor invàlida " -gotInvalidResponseErrorDescription: "No es pot contactar amb el servidor o potser es troba fora de línia per manteniment. Provar-ho de nou més tard." -thisPostMayBeAnnoying: "Aquesta nota pot ser molesta per algú." -thisPostMayBeAnnoyingHome: "Publicar a la línia de temps d'Inici" -thisPostMayBeAnnoyingCancel: "Cancel·lar " -thisPostMayBeAnnoyingIgnore: "Publicar de totes maneres" -collapseRenotes: "Col·lapsar les renotes que ja has vist" -internalServerError: "Error intern del servidor" -internalServerErrorDescription: "El servidor ha fallat de manera inexplicable." -copyErrorInfo: "Copiar la informació de l'error " -joinThisServer: "Registra't en aquesta instància " -exploreOtherServers: "Cerca una altra instància " -letsLookAtTimeline: "Dona una ullada a la línia de temps" -disableFederationConfirm: "Vols treure la federació?" -disableFederationConfirmWarn: "Fins i tot traient la federació, les publicacions continuaren sent públiques, a no ser que es digui el contrari. Normalment no has de tocar això." -disableFederationOk: "Desactivar" -invitationRequiredToRegister: "Aquesta instància només permet el registre per invitació. Per registrar-te has d'introduir el codi d'invitació." -emailNotSupported: "Aquesta instància no suporta l'enviament de correus electrònics " -postToTheChannel: "Publicar a un Canal" -cannotBeChangedLater: "Això ja no es podrà canviar." -reactionAcceptance: "Acceptació de reaccions " -likeOnly: "Només m'agraden " -likeOnlyForRemote: "Tot (només m'agraden d'instàncies remotes)" -nonSensitiveOnly: "Només sense contingut sensible" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Només contingut no sensible (Només m'agraden d'instàncies remotes)" -rolesAssignedToMe: "Rols assignats " -resetPasswordConfirm: "Vols canviar la teva contrasenya?" -sensitiveWords: "Paraules sensibles" -sensitiveWordsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves." -sensitiveWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular." -prohibitedWords: "Paraules prohibides" -prohibitedWordsDescription: "Quan intenteu publicar una Nota que conté una paraula prohibida, feu que es converteixi en un error. Es poden dividir i establir múltiples línies." -prohibitedWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular." -hiddenTags: "Etiquetes ocultes" -hiddenTagsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves." -notesSearchNotAvailable: "La cerca de notes no es troba disponible." -license: "Llicència" -unfavoriteConfirm: "Esborrar dels favorits?" -myClips: "Els meus retalls" -drivecleaner: "Netejador de Disc" -retryAllQueuesNow: "Prova de nou d'executar totes les cues" -retryAllQueuesConfirmTitle: "Tornar a intentar-ho tot?" -retryAllQueuesConfirmText: "Això farà que la càrrega del servidor augmenti temporalment." -enableChartsForRemoteUser: "Generar gràfiques d'usuaris remots" -enableChartsForFederatedInstances: "Generar gràfiques d'instàncies remotes" -showClipButtonInNoteFooter: "Afegir \"Retall\" al menú d'acció de la nota" -reactionsDisplaySize: "Mida de les reaccions" -limitWidthOfReaction: "Limitar l'amplada màxima de la reacció i mostrar-les en una mida reduïda " -noteIdOrUrl: "ID o URL de la nota" -video: "Vídeo" -videos: "Vídeos " -audio: "So" -audioFiles: "So" -dataSaver: "Economitzador de dades" -accountMigration: "Migració del compte" -accountMoved: "Aquest usuari té un compte nou:" -accountMovedShort: "Aquest compte ha sigut migrat" -operationForbidden: "Operació no permesa " -forceShowAds: "Mostra els anuncis sempre " -addMemo: "Afegir recordatori" -editMemo: "Editar recordatori" -reactionsList: "Reaccions" -renotesList: "Impulsos" -notificationDisplay: "Notificacions" -leftTop: "Dalt a l'esquerra " -rightTop: "Dalt a la dreta " -leftBottom: "A baix a l'esquerra" -rightBottom: "A baix a la dreta" -stackAxis: "Apilar en direcció " -vertical: "Vertical" -horizontal: "Horitzontal " -position: "Posició " -serverRules: "Regles del servidor" -pleaseConfirmBelowBeforeSignup: "Per obrir un compte en aquest servidor, has de llegir i acceptar el següent." -pleaseAgreeAllToContinue: "Has d'acceptar tots els camps de dalt per poder continuar." -continue: "Continuar" -preservedUsernames: "Noms d'usuaris reservats" -preservedUsernamesDescription: "Llistat de noms d'usuaris que no es poden fer servir separats per salts de linia. Aquests noms d'usuaris no estaran disponibles quan es creï un compte d'usuari normal, però els administradors els poden fer servir per crear comptes manualment. Per altre banda els comptes ja creats amb aquests noms d'usuari no es veure'n afectats." -createNoteFromTheFile: "Compon una nota des d'aquest fitxer" -archive: "Arxiu" -channelArchiveConfirmTitle: "Vols arxivar {name}?" -channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista de canals o als resultats de cerca. Tampoc es poden afegir noves entrades." -thisChannelArchived: "Aquest Canal ha sigut arxivat." -displayOfNote: "Mostrar notes" -initialAccountSetting: "Configuració del perfil" -youFollowing: "Seguit" -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" -specifyUser: "Especificar usuari" -failedToPreviewUrl: "Vista prèvia no disponible" -update: "Actualitzar" -rolesThatCanBeUsedThisEmojiAsReaction: "Rols que poden fer servir aquest emoji com a reacció " -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si cap rol es especificat tothom ho pot fer servir" -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Aquests rols han de ser públics " -cancelReactionConfirm: "Vols esborrar la teva reacció?" -changeReactionConfirm: "Vols canviar la teva reacció?" -later: "Més tard" -goToMisskey: "Ves a CherryPick" -additionalEmojiDictionary: "Diccionari d'emojis adicionals" -installed: "Instal·lats " -branding: "Marca" -enableServerMachineStats: "Publicar estadístiques del maquinari del servidor" -enableIdenticonGeneration: "Activar la generació d'icones d'identificació " -turnOffToImprovePerformance: "Desactivant aquesta opció es pot millorar el rendiment." -createInviteCode: "Crear codi d'invitació " -createWithOptions: "Crear invitació amb opcions" -createCount: "Comptador d'invitacions " -inviteCodeCreated: "Invitació creada" -inviteLimitExceeded: "Has sobrepassat el límit d'invitacions que pots crear." -createLimitRemaining: "Et queden {limit} invitacions restants" -inviteLimitResetCycle: "Cada {time} {limit} invitacions." -expirationDate: "Data de venciment" -noExpirationDate: "Sense data de venciment" -inviteCodeUsedAt: "Codi d'invitació fet servir el" -registeredUserUsingInviteCode: "Codi d'invitació fet servir per l'usuari " -waitingForMailAuth: "Esperant la verificació per correu electrònic " -inviteCodeCreator: "Invitació creada per" -usedAt: "Utilitzada el" -unused: "Sense utilitzar" -used: "Utilitzada" -expired: "Caducat" -doYouAgree: "Estàs d'acord?" -beSureToReadThisAsItIsImportant: "Llegeix això perquè és molt important." -iHaveReadXCarefullyAndAgree: "He llegit {x} i estic d'acord." -dialog: "Diàleg " icon: "Icona" -forYou: "Per a tu" -currentAnnouncements: "Informes actuals" -pastAnnouncements: "Informes passats" -youHaveUnreadAnnouncements: "Tens informes per llegir." -useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey." replies: "Respondre" renotes: "Impulsa" -loadReplies: "Mostrar les respostes" -loadConversation: "Mostrar la conversació " -pinnedList: "Llista fixada" -keepScreenOn: "Mantenir la pantalla encesa" -verifiedLink: "La propietat de l'enllaç ha sigut verificada" -notifyNotes: "Notificar quan hi hagi notes noves" -unnotifyNotes: "Deixar de notificar quan hi hagi notes noves" -authentication: "Autenticació " -authenticationRequiredToContinue: "Si us plau autentificat per continuar" -dateAndTime: "Data i hora" -showRenotes: "Mostrar impulsos" -edited: "Editat" -notificationRecieveConfig: "Paràmetres de notificacions" -mutualFollow: "Seguidor mutu" -followingOrFollower: "Seguit 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" -showRepliesToOthersInTimelineAll: "Mostrar les respostes a altres a usuaris que segueixes a la línia de temps" -hideRepliesToOthersInTimelineAll: "Ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps" -confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les teves respostes a tots els que segueixes a la teva línia de temps?" -confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?" -externalServices: "Serveis externs" -sourceCode: "Codi font" -repositoryUrl: "URL del repositori" -feedback: "Opinió" -feedbackUrl: "URL per a opinar" -impressum: "Impressum" -impressumUrl: "Adreça URL impressum" -impressumDescription: "A països, com Alemanya, la inclusió de la informació de contacte de l'operador (un Impressum) és requereix de manera legal per llocs comercials." -privacyPolicy: "Política de privacitat" -privacyPolicyUrl: "Adreça URL de la política de privacitat" -tosAndPrivacyPolicy: "Termes d'ús i política de privacitat" -avatarDecorations: "Decoracions dels avatars" -attach: "Adjuntar" -detach: "Eliminar" -detachAll: "Treure tot" -angle: "Angle" -flip: "Girar" -showAvatarDecorations: "Mostrar les decoracions dels avatars" -releaseToRefresh: "Deixar anar per actualitzar" -refreshing: "Recarregant..." -pullDownToRefresh: "Llisca cap a baix per recarregar" -disableStreamingTimeline: "Desactivar l'actualització en temps real de les línies de temps" -useGroupedNotifications: "Mostrar les notificacions agrupades " -signupPendingError: "Hi ha hagut un problema verificant l'adreça de correu electrònic. L'enllaç pot haver caducat." -cwNotationRequired: "Si està activat \"Amagar contingut\" s'ha d'escriure una descripció " -doReaction: "Afegeix una reacció " -code: "Codi" -reloadRequiredToApplySettings: "És necessari recarregar la pàgina per aplicar els canvis." -remainingN: "Queden: {n}" -overwriteContentConfirm: "Vols substituir el contingut actual?" -seasonalScreenEffect: "Efectes de pantalla segons les estacions" -decorate: "Decorar" -addMfmFunction: "Afegeix funcions MFM" -enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions MFM" -bubbleGame: "Bubble Game" -sfx: "Efectes de so" -soundWillBePlayed: "Es reproduiran efectes de so" -showReplay: "Veure reproducció" -replay: "Reproduir" -replaying: "Reproduint" -endReplay: "Tanca la redifusió" -ranking: "Classificació" -lastNDays: "Últims {n} dies" -backToTitle: "Torna al títol" -hemisphere: "Geolocalització" -withSensitive: "Incloure notes amb fitxers sensibles" -userSaysSomethingSensitive: "La publicació de {name} conte material sensible" -enableHorizontalSwipe: "Lliscar per canviar de pestanya" -loading: "S’està carregant" -surrender: "Cancel·lar " -gameRetry: "Torna a provar" -notUsePleaseLeaveBlank: "Si no voleu usar-ho, deixeu-ho en blanc" -useTotp: "Usa una contrasenya d'un sol ús" -useBackupCode: "Usa un codi de recuperació" -_delivery: - stop: "Suspés" - _type: - none: "S'està publicant" -_bubbleGame: - howToPlay: "Com es juga" - _howToPlay: - section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa." - section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts." - section3: "El joc s'acabarà quan els objectes sobresurtin de la caixa. Intenta aconseguir la puntuació més gran possible fusionant objectes mentre impedeixes que sobresurtin de la caixa!" -_announcement: - forExistingUsers: "Anunci per usuaris registrats" - forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació." - needConfirmationToRead: "Es necessita confirmació de lectura de la notificació " - needConfirmationToReadDescription: "Si s'activa es mostrarà un diàleg per confirmar la lectura d'aquesta notificació. A més aquesta notificació serà exclosa de qualsevol funcionalitat com \"Marcar tot com a llegit\"." - end: "Final de la notificació " - tooManyActiveAnnouncementDescription: "Tenir massa notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els anuncis que siguin antics." - readConfirmTitle: "Marcar com llegida?" - readConfirmText: "Això marcarà el contingut de \"{title}\" com llegit." - shouldNotBeUsedToPresentPermanentInfo: "Ja que l'ús de notificacions pot impactar l'experiència dels nous usuaris, és recomanable fer servir les notificacions amb el flux d'informació en comptes de fer-les servir en un únic bloc." - dialogAnnouncementUxWarn: "Tenir dues o més notificacions amb l'estil de finestres pot impactar l'experiència de l'usuari, és per això que és recomana fer-lo servir amb cura." - silence: "Sense notificacions" - silenceDescription: "Activant aquesta opció la notificació no es mostrarà ni l'usuari l'haurà de llegir." -_initialAccountSetting: - accountCreated: "S'ha completat la creació del compte!" - letsStartAccountSetup: "Posem ràpidament la configuració inicial del compte." - letsFillYourProfile: "Comencem establint el teu perfil." - profileSetting: "Configuració del perfil" - privacySetting: "Configuració de seguretat" - theseSettingsCanEditLater: "Aquests ajustos es poden canviar més tard." - youCanEditMoreSettingsInSettingsPageLater: "A més d'això, es poden fer diferents configuracions a través de la pàgina de configuració. Assegureu-vos de comprovar-ho més tard." - followUsers: "Prova de seguir usuaris que t'interessin per construir la teva línia de temps." - pushNotificationDescription: "Activant les notificacions emergents et permetrà rebre notificacions de {name} directament al teu dispositiu." - initialAccountSettingCompleted: "Configuració del perfil completada!" - haveFun: "Disfruta {name}!" - youCanContinueTutorial: "Pots continuar amb un tutorial per aprendre a Fer servir {name} (CherryPick) o tu pots estalviar i començar a fer-lo servir ja." - startTutorial: "Començar el tutorial" - skipAreYouSure: "Et vols saltar la configuració del perfil?" - laterAreYouSure: "Vols continuar la configuració del perfil més tard?" -_initialTutorial: - launchTutorial: "Començar tutorial" - title: "Tutorial" - wellDone: "Ben fet!" - skipAreYouSure: "Sortir del tutorial?" - _landing: - title: "Benvingut al tutorial" - description: "Aquí aprendràs el bàsic per poder fer servir CherryPick i les seves característiques." - _note: - title: "Què és una Nota?" - description: "Les publicacions a CherryPick es diuen 'Notes'. Les Notes s'ordenen cronològicament a la línia de temps i s'actualitzen de forma automàtica." - reply: "Fes clic en aquest botó per contestar a un missatge. També és possible contestar a una contestació, continuant la conversació en forma de fil." - renote: "Pots compartir una Nota a la teva pròpia línia de temps. Inclús pots citar-les amb els teus comentaris." - reaction: "Pots afegir reaccions a les Notes. Entrarem més en detall a la pròxima pàgina." - menu: "Pots veure els detalls de les Notes, copiar enllaços i fer diferents accions." - _reaction: - title: "Què són les Reaccions?" - description: "Es poden reaccionar a les Notes amb diferents emoticones. Les reaccions et permeten expressar matisos que hi són més enllà d'un simple m'agrada." - letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!" - reactToContinue: "Afegeix una reacció per continuar." - reactNotification: "Rebràs notificacions en temps real quan un usuari reaccioni a les teves notes." - reactDone: "Pots desfer una reacció fent clic al botó '-'." - _timeline: - title: "El concepte de les línies de temps" - description1: "CherryPick mostra diferents línies de temps basades en l'ús (algunes poden no estar disponibles depenent de la política del servidor)" - home: "Pots veure notes dels comptes que segueixes" - local: "Pots veure les notes dels usuaris del servidor." - social: "Es mostren les notes de les línies de temps d'Inici i Local." - global: "Pots veure les notes de tots els servidors connectats." - description2: "Pots canviar la línia de temps en qualsevol moment fent servir la barra de la pantalla superior." - description3: "A més hi ha línies de temps per llistes i per canals. Si vols saber més {link}." - _postNote: - title: "Configuració de la publicació de les notes" - description1: "Quan públiques una nota a CherryPick hi ha diferents opcions disponibles. El formulari de publicació es veu així" - _visibility: - description: "Pots limitar qui pot veure les teves notes." - public: "La teva nota serà visible per a tots els usuaris." - home: "Publicar només a línia de temps d'Inici. La gent que visiti el teu perfil o mitjançant les remotes també la podran veure." - followers: "Només visible per a seguidors. Només els teus seguidors la podran veure i ningú més. Ningú més podrà fer renotes." - direct: "Només visible per a alguns seguidors, el destinatari rebre una notificació. Es pot fer servir com una alternativa als missatges directes." - doNotSendConfidencialOnDirect1: "Tingues cura quan enviïs informació sensible." - doNotSendConfidencialOnDirect2: "Els administradors del servidor poden veure tot el que escrius. Ves compte quan enviïs informació sensible en enviar notes directes a altres usuaris en servidors de poca confiança." - localOnly: "Publicar amb aquesta opció activada farà que la nota no federi amb altres servidors. Els usuaris d'altres servidors no podran veure la nota directament, sense importar les opcions de visualització." - _cw: - title: "Avís de Contingut (CW)" - description: "En comptes del cos de la nota es mostrarà el que s'escrigui al camp de 'comentaris'. Fent clic a 'Llegir més' es mostrarà el cos." - _exampleNote: - cw: "Això et farà venir gana!" - note: "Acabo de menjar-me un donut de xocolata 🍩😋" - useCases: "Això es fa servir per seguir normes del servidor sobre certes notes o per ocultar contingut sensible O revelador." - _howToMakeAttachmentsSensitive: - title: "Com marcar adjunts com a contingut sensible?" - description: "Per adjunts que sigui requerit per les normes del servidor o que puguin contenir material sensible, s'ha d'afegir l'opció 'sensible'." - tryThisFile: "Prova de marcar la imatge adjunta en aquest formulari com a sensible!" - _exampleNote: - note: "Oops! L'he fet bona en obrir la tapa de Nocilla..." - method: "Per marcar un adjunt com a sensible, fes clic a la miniatura de l'adjunt, obre el menú i fes clic a 'Marcar com a sensible'." - sensitiveSucceeded: "Quan adjuntis fitxers si us plau marca la sensibilitat seguint les normes del servidor." - doItToContinue: "Marca el fitxer adjunt com a sensible per poder continuar." - _done: - title: "Has completat el tutorial 🎉" - description: "Les funcions explicades aquí és una petita mostra. Per una explicació més detallada de com fer servir CherryPick consulta {link}." -_timelineDescription: - home: "A la línia de temps d'Inici pots veure les notes dels usuaris que segueixes." - local: "A la línia de temps Local pots veure les notes de tots els usuaris d'aquest servidor." - social: "La línia de temps Social mostren les notes de les línies de temps d'Inici i Local." - global: "A la línia de temps Global pots veure les notes de tots els servidors connectats." -_serverRules: - description: "Un conjunt de regles que seran mostrades abans de registrar-se. Es recomanable configurar un resum dels termes d'ús." -_serverSettings: - iconUrl: "URL de la icona" - appIconDescription: "Especifica la icona que es mostrarà quan el {host} es mostri en una aplicació." - appIconUsageExample: "Per exemple com a PWA, o quan es mostri com un favorit a la pàgina d'inici del telèfon mòbil" - appIconStyleRecommendation: "Com la icona pot ser retallada com un cercle o un quadrat, es recomana fer servir una icona amb un marge acolorit que l'envolti." - appIconResolutionMustBe: "La resolució mínima és {resolution}." - manifestJsonOverride: "Sobreescriure manifest.json" - shortName: "Nom curt" - shortNameDescription: "Una abreviatura del nom de la instància que es poguí mostrar en cas que el nom oficial sigui massa llarg" - fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les línies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat." - fanoutTimelineDbFallback: "Carregar de la base de dades" - fanoutTimelineDbFallbackDescription: "Quan s'activa, la línia de temps fa servir la base de dades per consultes adicionals si la línia de temps no es troba a la memòria cau. Si és desactiva la càrrega del servidor és veure reduïda, però també és reduirà el nombre de línies de temps que és poden obtenir." -_accountMigration: - moveFrom: "Migrar un altre compte a aquest" - moveFromSub: "Crear un àlies per un altre compte" - moveFromLabel: "Compte original #{n}" - moveFromDescription: "Has de crear un àlies del compte que vols migrar en aquest compte.\nFes servir aquest format per posar el compte que vols migrar: @nomusuari@servidor.exemple.com\nPer esborrar l'àlies deixa el camp en blanc (no és recomanable de fer)" - moveTo: "Migrar aquest compte a un altre" - moveToLabel: "Compte al qual es vol migrar:" - moveCannotBeUndone: "Les migracions dels comptes no es poden desfer." - moveAccountDescription: "Això migrarà la teva compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a CherryPick v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)" - moveAccountHowTo: "Per fer la migració, primer has de crear un àlies per aquest compte al compte al qual vols migrar.\nDesprés de crear l'àlies, introdueix el compte al qual vols migrar amb el format següent: @nomusuari@servidor.exemple.com" - 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." - movedTo: "Nou compte:" -_achievements: - earnedAt: "Desbloquejat el" - _types: - _notes1: - title: "Aquí, configurant el meu msky" - description: "Publica la teva primera Nota" - flavor: "Passa-t'ho bé fent servir Miskey!" - _notes10: - title: "Algunes notes" - description: "Publica 10 notes" - _notes100: - title: "Un piló de notes" - description: "Publica 100 notes" - _notes500: - title: "Cobert de notes" - description: "Publica 500 notes" - _notes1000: - title: "Un piló de notes" - description: "1 000 notes publicades" - _notes5000: - title: "Desbordament de notes" - description: "5 000 notes publicades" - _notes10000: - title: "Supernota" - description: "10 000 notes publicades" - _notes20000: - title: "Necessito... Més... Notes!" - description: "20 000 notes publicades" - _notes30000: - title: "Notes notes notes!" - description: "30 000 notes publicades" - _notes40000: - title: "Fàbrica de notes" - description: "40 000 notes publicades" - _notes50000: - title: "Planeta de notes" - description: "50 000 notes publicades" - _notes60000: - title: "Quàsar de notes" - description: "60 000 notes publicades" - _notes70000: - title: "Forat negre de notes" - description: "70 000 notes publicades" - _notes80000: - title: "Galàxia de notes" - description: "80 000 notes publicades" - _notes90000: - title: "Univers de notes" - description: "90 000 notes publicades" - _notes100000: - title: "ALL YOUR NOTE ARE BELONG TO US" - description: "100 000 notes publicades" - flavor: "Segur que tens moltes coses a dir?" - _login3: - title: "Principiant I" - description: "Vas iniciar sessió fa tres dies" - flavor: "Des d'avui diguem Cherrypikist" - _login7: - title: "Principiant II" - description: "Vas iniciar sessió fa set dies" - flavor: "Ja saps com va funcionant tot?" - _login15: - title: "Principiant III" - description: "Vas iniciar sessió fa quinze dies" - _login30: - title: "Misskist I" - description: "Vas iniciar sessió fa trenta dies" - _login60: - title: "Misskist II" - description: "Vas iniciar sessió fa seixanta dies" - _login100: - title: "Misskist III" - description: "Vas iniciar sessió fa cent dies" - flavor: "Misskist violent" - _login200: - title: "Regular I" - description: "Vas iniciar sessió fa dos-cents dies" - _login300: - title: "Regular II" - description: "Vas iniciar sessió fa tres-cents dies" - _login400: - title: "Regular III" - description: "Vas iniciar sessió fa quatre-cents dies" - _login500: - title: "Expert I" - description: "Vas iniciar sessió fa cinc-cents dies" - flavor: "Amics, he dit massa vegades que soc un amant de les notes" - _login600: - title: "Expert II" - description: "Vas iniciar sessió fa sis-cents dies" - _login700: - title: "Expert III" - description: "Vas iniciar sessió fa set-cents dies" - _login800: - title: "Mestre de les Notes I" - description: "Vas iniciar sessió fa vuit-cents dies " - _login900: - title: "Mestre de les Notes II" - description: "Vas iniciar sessió fa nou-cents dies" - _login1000: - title: "Mestre de les Notes III" - description: "Vas iniciar sessió fa mil dies" - flavor: "Gràcies per fer servir CherryPick!" - _noteClipped1: - title: "He de retallar-te!" - description: "Retalla la teva primera nota" - _noteFavorited1: - title: "Quan miro les estrelles" - description: "La primera vegada que vaig registrar el meu favorit" - _myNoteFavorited1: - title: "Vull una estrella" - description: "La meva nota va ser registrada com favorita per una de les altres persones" - _profileFilled: - title: "Estic a punt" - description: "Vaig fer la configuració de perfil" - _markedAsCat: - title: "Soc un gat" - description: "He establert el meu compte com si fos un Gat" - flavor: "Encara no tinc nom" - _following1: - title: "És el meu primer seguiment" - description: "És la primera vegada que et segueixo" - _following10: - title: "Segueix-me... Segueix-me..." - description: "Seguir 10 usuaris" - _following50: - title: "Molts amics" - description: "Seguir 50 comptes" - _following100: - title: "100 amics" - description: "Segueixes 100 comptes" - _following300: - title: "Sobrecàrrega d'amics" - description: "Segueixes 300 comptes" - _followers1: - title: "Primer seguidor" - description: "1 seguidor guanyat" - _followers10: - title: "Segueix-me!" - description: "10 seguidors guanyats" - _followers50: - title: "Venen en manada" - description: "50 seguidors guanyats" - _followers100: - title: "Popular" - description: "100 seguidors guanyats" - _followers300: - title: "Si us plau, d'un en un!" - description: "300 seguidors guanyats" - _followers500: - title: "Torre de ràdio" - description: "500 seguidors guanyats" - _followers1000: - title: "Influenciador" - description: "1 000 seguidors guanyats" - _collectAchievements30: - title: "Col·leccionista d'èxits " - description: "Desbloqueja 30 assoliments" - _viewAchievements3min: - title: "M'agraden els èxits " - description: "Mira la teva llista d'assoliments durant més de 3 minuts" - _iLoveCherryPick: - title: "Estimo CherryPick" - description: "Publica \"I ❤ #CherryPick\"" - flavor: "L'equip de desenvolupament de CherryPick agraeix el vostre suport!" - _foundTreasure: - title: "A la Recerca del Tresor" - description: "Has trobat el tresor amagat" - _client30min: - title: "Parem una estona" - description: "Mantingues obert CherryPick per 30 minuts" - _client60min: - title: "A totes amb CherryPick" - description: "Mantingues CherryPick obert per 60 minuts" - _noteDeletedWithin1min: - title: "No et preocupis" - description: "Esborra una nota al minut de publicar-la" - _postedAtLateNight: - title: "Nocturn" - description: "Publica una nota a altes hores de la nit " - flavor: "És hora d'anar a dormir." - _postedAt0min0sec: - title: "Rellotge xerraire" - description: "Publica una nota a les 0:00" - flavor: "Tic tac, tic tac, tic tac, DING!" - _selfQuote: - title: "Autoreferència " - description: "Cita una nota teva" - _htl20npm: - title: "Línia de temps fluida" - description: "La teva línia de temps va a més de 20npm (notes per minut)" - _viewInstanceChart: - title: "Analista " - description: "Mira els gràfics de la teva instància " - _outputHelloWorldOnScratchpad: - title: "Hola, món!" - description: "Escriu \"hola, món\" al bloc de notes" - _open3windows: - title: "Multi finestres" - description: "I va obrir més de tres finestres" - _driveFolderCircularReference: - title: "Consulteu la secció de bucle" - description: "Intenta crear carpetes recursives al Disc" - _reactWithoutRead: - title: "De veritat has llegit això?" - description: "Reaccions a una nota de més de 100 caràcters publicada fa menys de 3 segons " - _clickedClickHere: - title: "Fer clic" - description: "Has fet clic aquí " - _justPlainLucky: - title: "Ha sigut sort" - description: "Oportunitat de guanyar-lo amb una probabilitat d'un 0.005% cada 10 segons" - _setNameToSyuilo: - title: "soc millor" - description: "Posat \"siuylo\" com a nom" - _passedSinceAccountCreated1: - title: "Primer aniversari" - description: "Ja ha passat un any d'ençà que vas crear el teu compte" - _passedSinceAccountCreated2: - title: "Segon aniversari" - description: "Ja han passat dos anys d'ençà que vas crear el teu compte" - _passedSinceAccountCreated3: - title: "Tres anys" - description: "Ja han passat tres anys d'ençà que vas crear el teu compte" - _loggedInOnBirthday: - title: "Felicitats!" - description: "T'has identificat el dia del teu aniversari" - _loggedInOnNewYearsDay: - title: "Bon any nou!" - description: "T'has identificat el primer dia de l'any " - flavor: "A per un altre any memorable a la teva instància " - _cookieClicked: - title: "Un joc en què fas clic a les galetes" - description: "Pica galetes" - flavor: "Espera, ets al lloc web correcte?" - _brainDiver: - title: "Busseja Ments" - description: "Publica un enllaç al Busseja Ments" - flavor: "Misskey-Misskey La-Tu-Ma" - _smashTestNotificationButton: - title: "Sobrecàrrega de proves" - description: "Envia moltes notificacions de prova en un període de temps molt curt" - _tutorialCompleted: - title: "Diploma del Curs Elemental de CherryPick" - description: "Has completat el tutorial" - _bubbleGameExplodingHead: - title: "🤯" - description: "L'objecte més gran del joc de la bombolla " - _bubbleGameDoubleExplodingHead: - title: "Doble 🤯" - description: "Dos dels objectes més grans del joc de la bombolla al mateix temps" - flavor: "Pots emplenar una carmanyola com aquesta 🤯🤯 una mica" _role: - new: "Nou rol" - edit: "Editar el rol" - name: "Nom del rol" - description: "Descripció del rol" - permission: "Permisos de rol" - descriptionOfPermission: "Els Moderadors poden fer operacions bàsiques de moderació.\nEls Administradors poden canviar tots els ajustos del servidor." - assignTarget: "Assignar " - descriptionOfAssignTarget: "Manual per canviar manualment qui és part d'aquest rol i qui no.\nCondicional per afegir o eliminar de manera automàtica els usuaris d'aquest rol basat en una determinada condició." - manual: "Manual" - manualRoles: "Rols manuals" - conditional: "Condicional" - conditionalRoles: "Rols condicionals" - condition: "Condició" - isConditionalRole: "Aquest és un rol condicional" - isPublic: "Rol públic" - descriptionOfIsPublic: "Aquest rol es mostrarà al perfil dels usuaris al que se'ls assigni." - options: "Opcions" - policies: "Polítiques" - baseRole: "Plantilla de rols" - useBaseValue: "Fer servir els valors de la plantilla de rols" - chooseRoleToAssign: "Selecciona els rols a assignar" - iconUrl: "URL de la icona " - asBadge: "Mostrar com a insígnia " - descriptionOfAsBadge: "La icona d'aquest rol es mostrarà al costat dels noms d'usuaris que tinguin assignats aquest rol." - isExplorable: "Fer el rol explorable" - descriptionOfIsExplorable: "La línia de temps d'aquest rol i la llista d'usuaris seran públics si s'activa." - displayOrder: "Posició " - descriptionOfDisplayOrder: "Com més gran és el número, més dalt la seva posició a la interfície." - canEditMembersByModerator: "Permetre que els moderadors editin la llista d'usuaris en aquest rol" - descriptionOfCanEditMembersByModerator: "Quan s'activa, els moderadors, així com els administradors, podran afegir i treure usuaris d'aquest rol. Si es troba desactivat, només els administradors poden assignar usuaris." - priority: "Prioritat" _priority: - low: "Baixa" middle: "Mitjà" - high: "Alta" _options: - gtlAvailable: "Pot veure la línia de temps global" - ltlAvailable: "Pot veure la línia de temps local" - canPublicNote: "Pot enviar notes públiques" - canInvite: "Pot crear invitacions a la instància " - inviteLimit: "Límit d'invitacions " - inviteLimitCycle: "Temps de refresc de les invitacions" - inviteExpirationTime: "Interval de caducitat de les invitacions" - canManageCustomEmojis: "Gestiona els emojis personalitzats" - canManageAvatarDecorations: "Gestiona les decoracions dels avatars " - driveCapacity: "Capacitat del disc" - alwaysMarkNsfw: "Marca sempre els fitxers com a sensibles" - pinMax: "Nombre màxim de notes fixades" antennaMax: "Nombre màxim d'antenes" - wordMuteMax: "Nombre màxim de caràcters permesos a les paraules silenciades" - webhookMax: "Nombre màxim de Webhooks" - clipMax: "Nombre màxim de clips" - noteEachClipsMax: "Nombre màxim de notes dintre d'un clip" - userListMax: "Nombre màxim de llistes d'usuaris " - userEachUserListsMax: "Nombre màxim d'usuaris dintre d'una llista d'usuaris " - rateLimitFactor: "Limitador" - descriptionOfRateLimitFactor: "Límits baixos són menys restrictius, límits alts són més restrictius." - canHideAds: "Pot amagar els anuncis" - canSearchNotes: "Pot cercar notes" - canUseTranslator: "Pot fer servir el traductor" - avatarDecorationLimit: "Nombre màxim de decoracions que es poden aplicar els avatars" - _condition: - isLocal: "Usuari local" - isRemote: "Usuari remot" - createdLessThan: "Han passat menys de X a passat des de la creació del compte" - createdMoreThan: "Han passat més de X des de la creació del compte" - followersLessThanOrEq: "Té menys de X seguidors" - followersMoreThanOrEq: "Té X o més seguidors" - followingLessThanOrEq: "Segueix X o menys comptes" - followingMoreThanOrEq: "Segueix a X o més comptes" - notesLessThanOrEq: "Les publicacions són menys o igual a " - notesMoreThanOrEq: "Les publicacions són més o igual a " - and: "AND condicional " - or: "OR condicional" - not: "NOT condicional" -_sensitiveMediaDetection: - description: "Redueix els esforços de moderació gràcies al reconeixement automàtic dels fitxers amb contingut sensible mitjançant Machine Learing. Això augmentarà la càrrega del servidor." - sensitivity: "Sensibilitat de la detecció " - sensitivityDescription: "Reduint la sensibilitat provocarà menys falsos positius. D'altra banda incrementant-ho generarà més falsos negatius." - setSensitiveFlagAutomatically: "Marcar com a sensible" - setSensitiveFlagAutomaticallyDescription: "Els resultats de la detecció interna seran desats, inclòs si aquesta opció es troba desactivada." - analyzeVideos: "Activar anàlisis de vídeos " - analyzeVideosDescription: "Analitzar els vídeos a més de les imatges. Això incrementarà lleugerament la càrrega del servidor." -_emailUnavailable: - used: "Aquest correu electrònic ja s'està fent servir" - format: "El format del correu electrònic és invàlid " - disposable: "No es poden fer servir adreces de correu electrònic d'un sol ús " - mx: "Aquest servidor de correu electrònic no és vàlid " - 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" - followers: "Visible només per a seguidors " - private: "Privat" -_signup: - almostThere: "Ja quasi estem" - emailAddressInfo: "Si us plau, escriu la teva adreça de correu electrònic. No es farà pública." - emailSent: "S'ha enviat un correu de confirmació a ({email}). Si us plau, fes clic a l'enllaç per completar el registre." -_accountDelete: - accountDelete: "Eliminar el compte" - mayTakeTime: "Com l'eliminació d'un compte consumeix bastants recursos, pot trigar un temps perquè es completi l'esborrat, depenent si tens molt contingut i la quantitat de fitxer que hagis pujat." - sendEmail: "Una vegada hagi finalitzat l'esborrat del compte rebràs un correu electrònic a l'adreça que tinguis registrada en aquest compte." - requestAccountDelete: "Demanar l'eliminació del compte" - started: "Ha començat l'esborrat del compte." - inProgress: "L'esborrat es troba en procés " -_ad: - back: "Tornar" - reduceFrequencyOfThisAd: "Mostrar menys aquest anunci" - hide: "No mostrar mai" - timezoneinfo: "El dia de la setmana ve determinat del fus horari del servidor." - adsSettings: "Configuració d'anuncis " - notesPerOneAd: "Interval d'emplaçament d'anuncis en temps real (Notes per anuncis)" - setZeroToDisable: "Ajusta aquest valor a 0 per deshabilitar l'actualització d'anuncis en temps real" - adsTooClose: "L'interval actual pot fer que l'experiència de l'usuari sigui dolenta perquè l'interval és molt baix." -_forgotPassword: - enterEmail: "Escriu l'adreça de correu electrònic amb la que et vas registrar. S'enviarà un correu electrònic amb un enllaç perquè puguis canviar-la." - ifNoEmail: "Si no vas fer servir una adreça de correu electrònic per registrar-te, si us plau posa't en contacte amb l'administrador." - contactAdmin: "Aquesta instància no suporta registrar-se amb correu electrònic. Si us plau, contacta amb l'administrador del servidor." -_gallery: - my: "La meva Galeria " - liked: "Publicacions que t'han agradat" - like: "M'agrada " - unlike: "Ja no m'agrada" _email: _follow: title: "t'ha seguit" - _receiveFollowRequest: - title: "Has rebut una sol·licitud de seguiment" -_plugin: - install: "Instal·lar un afegit " - installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança." - manage: "Gestionar els afegits" - viewSource: "Veure l'origen " -_preferencesBackups: - list: "Llista de còpies de seguretat" - saveNew: "Fer una còpia de seguretat nova" - loadFile: "Carregar des d'un fitxer" - apply: "Aplicar en aquest dispositiu" - save: "Desar els canvis" - inputName: "Escriu un nom per aquesta còpia de seguretat" - cannotSave: "No s'ha pogut desar" - nameAlreadyExists: "Ja existeix una còpia de seguretat anomenada \"{name}\". Escriu un nom diferent." - applyConfirm: "Vols aplicar la còpia de seguretat \"{name}\" a aquest dispositiu? La configuració actual del dispositiu serà esborrada." - saveConfirm: "Desar còpia de seguretat com {name}?" - deleteConfirm: "Esborrar la còpia de seguretat {name}?" - renameConfirm: "Vols canvia el nom de la còpia de seguretat de \"{old}\" a \"{new}\"?" - noBackups: "No hi ha còpies de seguretat. Pots fer una còpia de seguretat de la configuració d'aquest dispositiu al servidor fent servir \"Crear nova còpia de seguretat\"" - createdAt: "Creat el: {date} {time}" - updatedAt: "Actualitzat el: {date} {time}" - cannotLoad: "Hi ha hagut un error al carregar" - invalidFile: "Format del fitxer no vàlid " -_registry: - scope: "Àmbit " - key: "Clau" - keys: "Claus" - domain: "Domini" - createKey: "Crear una clau" -_aboutMisskey: - about: "Misskey és un programa de codi obert desenvolupar per syuilo des de 2014" - contributors: "Col·laboradors principals" - allContributors: "Tots els col·laboradors " - source: "Codi font" - translation: "Tradueix Misskey" - donate: "Fes un donatiu a Misskey" - morePatrons: "També agraïm el suport d'altres col·laboradors que no surten en aquesta llista. Gràcies! 🥰" - patrons: "Patrocinadors" - projectMembers: "Membres del projecte" -_displayOfSensitiveMedia: - respect: "Ocultar imatges o vídeos marcats com a sensibles" - ignore: "Mostrar imatges o vídeos marcats com a sensibles" - force: "Ocultar totes les imatges o vídeos " -_instanceTicker: - none: "No mostrar mai" - remote: "Mostrar per usuaris remots" - always: "Mostrar sempre" -_serverDisconnectedBehavior: - reload: "Recarregar automàticament " - dialog: "Mostrar finestres de confirmació " - quiet: "Mostrar un avís que no molesti" -_channel: - create: "Crear un canal" - edit: "Editar canal" - setBanner: "Estableix el bàner " - removeBanner: "Eliminar el.bàner" - featured: "Popular" - owned: "Propietat" - following: "Seguin" - usersCount: "{n} Participants" - notesCount: "{n} Notes" - nameAndDescription: "Nom i descripció " - nameOnly: "Nom només " - allowRenoteToExternal: "Permet la citació i l'impuls fora del canal" -_menuDisplay: - sideFull: "Horitzontal " - sideIcon: "Horitzontal (icones)" - top: "A dalt" - hide: "Amagar" -_wordMute: - muteWords: "Paraules silenciades" - muteWordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR." - muteWordsDescription2: "Envolta les paraules amb barres per fer servir expressions regulars." _instanceMute: instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat." - instanceMuteDescription2: "Separar amb salts de línia" - title: "Ocultar notes de les instàncies en la llista." - heading: "Llista d'instàncies a silenciar" _theme: - explore: "Explorar els temes " - install: "Instal·lar un tema" - manage: "Gestionar els temes " - code: "Codi del tema" description: "Descripció" - installed: "{name} Instal·lat " - installedThemes: "Temes instal·lats " - builtinThemes: "Temes integrats" - alreadyInstalled: "Aquest tema ja es troba instal·lat " - invalid: "El format d'aquest tema no és correcte" - make: "Crear un tema" - base: "Base" - addConstant: "Afegir constant " - constant: "Constant" - defaultValue: "Valor per defecte" - color: "Color" - refProp: "Referència a una propietat" - refConst: "Referència a una constant " - key: "Clau" - func: "Funcions" - funcKind: "Tipus de funció " - argument: "Argument" - basedProp: "Propietat referenciada" - alpha: "Opacitat" - darken: "Enfosquir " - lighten: "Brillantor" - inputConstantName: "Escriu un nom per aquesta constant" - importInfo: "Si escrius el codi del tema aquí, el podràs importar a l'editor del tema" - deleteConstantConfirm: "Vols esborrar la constant {const}?" keys: - accent: "Accent" - bg: "Fons" - fg: "Text" - focus: "Enfocament" - indicator: "Indicador" - panel: "Taulell " - shadow: "Ombra" - header: "Capçalera" - navBg: "Fons de la barra lateral" - navFg: "Text de la barra lateral" - navHoverFg: "Text barra lateral (en passar per sobre)" - navActive: "Text barra lateral (actiu)" - navIndicator: "Indicador barra lateral" - link: "Enllaç" - hashtag: "Etiqueta" mention: "Menció" - mentionMe: "Mencions (jo)" renote: "Renotar" - modalBg: "Fons del modal" divider: "Divisor" - scrollbarHandle: "Maneta de la barra de desplaçament" - scrollbarHandleHover: "Maneta de la barra de desplaçament (en passar-hi per sobre)" - dateLabelFg: "Text de l'etiqueta de la data" - infoBg: "Fons d'informació " - infoFg: "Text d'informació " - infoWarnBg: "Fons avís " - infoWarnFg: "Text avís " - toastBg: "Fons notificació " - toastFg: "Text notificació " - buttonBg: "Fons botó " - buttonHoverBg: "Fons botó (en passar-hi per sobre)" - inputBorder: "Contorn del cap d'introducció " - listItemHoverBg: "Fons dels elements d'una llista" - driveFolderBg: "Fons de la carpeta Disc" - wallpaperOverlay: "Superposició del fons de pantalla " - badge: "Insígnia " - messageBg: "Fons del xat" - accentDarken: "Accent (fosc)" - accentLighten: "Accent (clar)" - fgHighlighted: "Text ressaltat" _sfx: note: "Notes" - noteMy: "Nota (per mi)" notification: "Notificacions" chat: "Xat" - reaction: "Quan se selecciona una reacció " -_soundSettings: - driveFile: "Fer servir un fitxer d'àudio del disc" - driveFileWarn: "Seleccionar un fitxer d'àudio del disc" - driveFileTypeWarn: "Fitxer no suportat " - driveFileTypeWarnDescription: "Seleccionar un fitxer d'àudio " - driveFileDurationWarn: "L'àudio és massa llarg" - driveFileDurationWarnDescription: "Els àudios molt llargs pot interrompre l'ús de CherryPick. Vols continuar?" -_ago: - future: "Futur " - justNow: "Ara mateix" - secondsAgo: "Fa {n} segons" - minutesAgo: "Fa {n} minuts" - hoursAgo: "Fa {n} hores" - daysAgo: "Fa {n} dies" - weeksAgo: "Fa {n} setmanes" - monthsAgo: "Fa {n} mesos" - yearsAgo: "Fa {n} anys" - invalid: "Res" -_timeIn: - seconds: "En {n} segons" - minutes: "En {n} minuts" - hours: "En {n} hores" - days: "En {n} dies" - weeks: "En {n} setmanes" - months: "En {n} mesos" - years: "En {n} anys" -_time: - second: "Segon(s)" - minute: "Minut(s)" - hour: "Hor(a)(es)" - day: "Di(a)(es)" + antenna: "Antenes" _2fa: - alreadyRegistered: "J has registrat un dispositiu d'autenticació de doble factor." - registerTOTP: "Registrar una aplicació autenticadora" - step1: "Primer instal·la una aplicació autenticadora (com {a} o {b}) al teu dispositiu." - step2: "Després escaneja el codi QR que es mostra en aquesta pantalla." - step2Uri: "Escriu la següent URI si estàs fent servir una aplicació d'escriptori " - step3Title: "Escriu un codi d'autenticació" - step3: "Escriu el codi d'autenticació (token) que es mostra a la teva aplicació per finalitzar la configuració." - setupCompleted: "Configuració terminada" - step4: "D'ara endavant quan accedeixis se't demanarà el token que has introduït." - securityKeyNotSupported: "El teu navegador no suporta claus de seguretat" - registerTOTPBeforeKey: "Configura una aplicació d'autenticació per registrar una clau de seguretat o una clau de pas." - securityKeyInfo: "A més de l'empremta digital o PIN per autenticar-te, pots configurar autenticació mitjançant maquinari que suporti claus de seguretat FIDO2, per protegir encara més el teu compte." - registerSecurityKey: "Registrar una clau de seguretat o clau de pas" - securityKeyName: "Escriu un nom per la clau" - tapSecurityKey: "Seguiu les instruccions del navegador i registrar les claus de seguretat o la clau de pas" - removeKey: "Esborrar la clau de seguretat" - removeKeyConfirm: "Esborrar la còpia de seguretat {name}?" - whyTOTPOnlyRenew: "L'aplicació d'autenticació no es pot eliminar mentre hi hagi una clau de seguretat registrada." - renewTOTP: "Reconfigurar l'aplicació d'autenticació " - renewTOTPConfirm: "Això farà que els codis de validació de l'antiga aplicació deixin de funcionar" - renewTOTPOk: "Reconfigurar" renewTOTPCancel: "No, gràcies" - checkBackupCodesBeforeCloseThisWizard: "Abans de tancar aquesta finestra, comprova el següent codi de seguretat." - backupCodes: "Codi de seguretat." - backupCodesDescription: "Si l'aplicació d'autenticació no es pot utilitzar, es pot accedir al compte utilitzant els següents codis de còpia de seguretat. Assegura't de mantenir aquests codis en un lloc segur. Cada codi es pot utilitzar només una vegada." - backupCodeUsedWarning: "Es va utilitzar un codi de còpia de seguretat. Si l'aplicació de certificació està disponible, reconfigura l'aplicació d'autenticació tan aviat com sigui possible." - backupCodesExhaustedWarning: "Es van utilitzar tots els codis de còpia de seguretat. Si no es pot utilitzar l'aplicació d'autenticació, ja no es pot accedir al compte. Torna a registrar l'aplicació d'autenticació." -_permissions: - "read:account": "Veure la informació del compte." - "write:account": "Editar la informació del compte." - "read:blocks": "Veure la llista d'usuaris bloquejats" - "write:blocks": "Editar la llista d'usuaris blocats" - "read:drive": "Accedeix als teus fitxers i carpetes del Disc" - "write:drive": "Editar o eliminar els teus fitxers i carpetes al Disc" - "read:favorites": "Veure la teva llista de favorits" - "write:favorites": "Editar la teva llista de favorits" - "read:following": "Veure informació de qui segueixes" - "write:following": "Segueix o deixa de seguir altres comptes" - "read:messaging": "Veure els teus xats" - "write:messaging": "Crear o esborrar missatges de xat" - "read:mutes": "Veure la teva llista d'usuaris silenciats" - "write:mutes": "Editar la teva llista d'usuaris silenciats" - "write:notes": "Crear o esborrar notes" - "read:notifications": "Veure les teves notificacions" - "write:notifications": "Gestionar les teves notificacions" - "read:reactions": "Veure les teves reaccions" - "write:reactions": "Editar les teves reaccions" - "write:votes": "Votar en una enquesta" - "read:pages": "Veure les teves pàgines " - "write:pages": "Editar o esborrar les teves pàgines " - "read:page-likes": "Veure la llista de les pàgines que t'han agradat" - "write:page-likes": "Editar la llista de les pàgines que t'han agradat" - "read:user-groups": "Veure els teus grups d'usuaris " - "write:user-groups": "Editar o esborrar els teus grups d'usuaris " - "read:channels": "Veure els teus canals" - "write:channels": "Editar els teus canals" - "read:gallery": "Veure la teva galeria " - "write:gallery": "Editar la teva galeria" - "read:gallery-likes": "Veure la llista de publicacions de galeries que t'han agradat" - "write:gallery-likes": "Editar la llista de publicacions de galeries que t'han agradat" - "read:flash": "Veure reproduccions" - "write:flash": "Editar reproduccions" - "read:flash-likes": "Veure la llista de reproduccions que t'han agradat" - "write:flash-likes": "Editar la llista de reproduccions que t'han agradat" - "read:admin:abuse-user-reports": "Veure informes d'usuaris " - "write:admin:delete-account": "Esborrar compte d'usuari " - "write:admin:delete-all-files-of-a-user": "Esborrar tots els fitxers d'un usuari" - "read:admin:index-stats": "Veure l'índex de la base de dades" - "read:admin:table-stats": "Veure la informació de les taules a la base de dades" - "read:admin:user-ips": "Veure adreça IP de l'usuari " - "read:admin:meta": "Veure meta-informació del servidor" - "write:admin:reset-password": "Reiniciar contrasenya d'usuari " - "write:admin:resolve-abuse-user-report": "Resoldre informes d'usuaris " - "write:admin:send-email": "Enviar correu electrònic " - "read:admin:server-info": "Veure informació del servidor" - "read:admin:show-moderation-log": "Veure registre de moderació " - "read:admin:show-user": "Veure informació privada de l'usuari " - "write:admin:suspend-user": "Suspendre usuari" - "write:admin:unset-user-avatar": "Esborrar avatar d'usuari " - "write:admin:unset-user-banner": "Esborrar bàner de l'usuari " - "write:admin:unsuspend-user": "Treure la suspensió d'un usuari" - "write:admin:meta": "Gestionar les metadades de la instància" - "write:admin:user-note": "Gestionar les notes de moderació " - "write:admin:roles": "Gestionar rols" - "read:admin:roles": "Veure rols" - "write:admin:relays": "Gestionar relé" - "read:admin:relays": "Veure relés" - "write:admin:invite-codes": "Gestionar codis d'invitació " - "read:admin:invite-codes": "Veure codis d'invitació " - "write:admin:announcements": "Gestionar anuncis" - "read:admin:announcements": "Veure anuncis" - "write:admin:avatar-decorations": "Gestionar la decoració dels avatars" - "read:admin:avatar-decorations": "Veure les decoracions dels avatars" - "write:admin:federation": "Gestionar la federació d'instàncies " - "write:admin:account": "Gestionar els comptes d'usuaris " - "read:admin:account": "Veure els comptes d'usuaris " - "write:admin:emoji": "Edició d'emojis" - "read:admin:emoji": "Veure emojis" - "write:admin:queue": "Gestionar la cua de feines" - "read:admin:queue": "Veure la cua de feines" _antennaSources: all: "Totes les publicacions" homeTimeline: "Publicacions dels usuaris seguits" @@ -2039,76 +581,18 @@ _widgets: timeline: "Línia de temps" activity: "Activitat" federation: "Federació" - button: "Botó " jobQueue: "Cua de tasques" _userList: chooseList: "Tria una llista" _cw: - hide: "Amagar" show: "Carregar més" - chars: "{count} caràcters " - files: "{count} fitxer(s)" -_poll: - noOnlyOneChoice: "Es necessita escollir dues opcions com a mínim " - choiceN: "Opció {n}" - noMore: "No pots afegir més opcions" - canMultipleVote: "Permetre escollir diferents opcions" - expiration: "Finalitza el" - infinite: "Mai" - at: "Finalitza en..." - after: "Finalitza després..." - deadlineDate: "Data de finalització " - deadlineTime: "Hor(a)(es)" - duration: "Duració " - votesCount: "{n} vots" - totalVotes: "{n} vots en total" - vote: "Votar en una enquesta" - showResult: "Veure resultats" - voted: "Has votat" - closed: "Finalitzada" - remainingDays: "Queden {d} dies i {h} hores per finalitzar" - remainingHours: "Queden {h} hores i {m} minuts" - remainingMinutes: "Queden {m} minuts i {s} segons" - remainingSeconds: "Queden {s} segons" _visibility: - public: "Públic " - publicDescription: "La teva nota la podrà veure tothom " home: "Inici" - homeDescription: "Publicar només a la línia de temps d'Inici " followers: "Seguidors" - followersDescription: "Fes només visible per als teus seguidors" - specified: "Directe" - specifiedDescription: "Fer visible només per alguns usuaris" - disableFederation: "Sense federar" - disableFederationDescription: "No enviar a altres servidors" -_postForm: - replyPlaceholder: "Contestar..." - quotePlaceholder: "Citar..." - channelPlaceholder: "Publicar a un canal..." - _placeholders: - a: "Que vols dir?..." - b: "Alguna cosa interessant al teu voltant?..." - c: "Què et passa pel cap?..." - d: "Què vols dir?..." - e: "Escriu alguna cosa..." - f: "Esperant que escriguis qualsevol cosa..." _profile: - name: "Nom" username: "Nom d'usuari" - description: "Biografia " - youCanIncludeHashtags: "Pots posar etiquetes a la teva biografia " - metadata: "Informació adicional " - metadataEdit: "Editar la informació adicional " - metadataDescription: "Amb això podràs mostrar camps d'informació adicional al teu perfil." - metadataLabel: "Etiqueta " - metadataContent: "Contingut" - changeAvatar: "Canviar l'avatar " - changeBanner: "Canviar el bàner " - verifiedLinkDescription: "Escrivint una adreça URL que enllaci a aquest perfil, una icona de propietat verificada es mostrarà al costat del camp." - avatarDecorationMax: "Pot afegir un màxim de {max} decoracions." _exportOrImport: allNotes: "Totes les publicacions" - clips: "Retalls" followingList: "Seguint" muteList: "Silencia" blockingList: "Bloqueja" @@ -2121,74 +605,18 @@ _timelines: social: "Social" global: "Global" _play: - viewSource: "Veure l'origen " - featured: "Popular" - title: "Títol " - script: "Script" summary: "Descripció" _pages: - viewSource: "Veure l'origen " - viewPage: "Veure les teves pàgines " - like: "M'agrada " - unlike: "Treure m'agrada " - my: "Les meves pàgines " - liked: "Pàgines que m'agraden " - featured: "Popular" - inspector: "Inspeccionar" contents: "Contingut" - content: "Bloquejar la pàgina " - variables: "Variables" - title: "Títol " - url: "URL de la pàgina " - summary: "Resum de la pàgina " - alignCenter: "Centrar elements" - hideTitleWhenPinned: "Amagar el títol de la pàgina quan estigui fixada al perfil" - font: "Lletra tipogràfica" - fontSerif: "Serif" - fontSansSerif: "Sans Serif" - eyeCatchingImageSet: "Escull una miniatura" - eyeCatchingImageRemove: "Esborrar la miniatura" - chooseBlock: "Afegeix un bloc" - selectType: "Seleccionar tipus" - contentBlocks: "Contingut" - inputBlocks: "Entrada " - specialBlocks: "Especial" blocks: - text: "Text" - textarea: "Àrea de text" - section: "Secció " image: "Imatges" - button: "Botó " - note: "Incorporar una Nota" _note: id: "ID de la publicació" - idDescription: "Alternativament pots enganxar l'adreça URL de la nota aquí." detailed: "Mostra els detalls" -_relayStatus: - requesting: "Pendent" - accepted: "Acceptat" - rejected: "Rebutjat" _notification: - fileUploaded: "Fitxer pujat sense cap problema" - youGotMention: "{name} t'ha mencionat" - youGotReply: "{name} t'ha contestat" - youGotQuote: "{name} t'ha citat" youRenoted: "Impulsat per {name}" youWereFollowed: "t'ha seguit" - youReceivedFollowRequest: "Has rebut una petició de seguiment" - yourFollowRequestAccepted: "La teva petició de seguiment ha sigut acceptada" - pollEnded: "Ja pots veure els resultats de l'enquesta " - newNote: "Nota nova" unreadAntennaNote: "Antena {name}" - roleAssigned: "Rol assignat " - emptyPushNotificationMessage: "Les notificacions han sigut actualitzades" - achievementEarned: "Aconseguiment desblocat" - testNotification: "Notificació de prova" - checkNotificationBehavior: "Comprova el comportament de la notificació " - sendTestNotification: "Enviar notificació de prova" - notificationWillBeDisplayedLikeThis: "Les notificacions és veure'n així " - reactedBySomeUsers: "Han reaccionat {n} usuaris" - renotedBySomeUsers: "L'han impulsat {n} usuaris" _types: all: "Tots" follow: "Seguint" @@ -2218,58 +646,8 @@ _deck: tl: "Línia de temps" antenna: "Antena" list: "Llistes" - channel: "Canals" mentions: "Mencions" direct: "Publicacions directes" -_webhookSettings: - name: "Nom" - active: "Activat" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Correu electrònic" _moderationLogTypes: suspend: "Suspèn" resetPassword: "Restableix la contrasenya" - suspendRemoteInstance: "Servidor remot suspès " - unsuspendRemoteInstance: "S'ha tret la suspensió del servidor remot" - markSensitiveDriveFile: "Fitxer marcat com a sensible" - unmarkSensitiveDriveFile: "S'ha tret la marca de sensible del fitxer" - resolveAbuseReport: "Informe resolt" - createInvitation: "Crear codi d'invitació " - createAd: "Anunci creat" - deleteAd: "Anunci esborrat" - updateAd: "Anunci actualitzat" - createAvatarDecoration: "Decoració de l'avatar creada" - updateAvatarDecoration: "S'ha actualitzat la decoració de l'avatar " - deleteAvatarDecoration: "S'ha esborrat la decoració de l'avatar " - unsetUserAvatar: "Esborrar l'avatar d'aquest usuari" - unsetUserBanner: "Esborrar el bàner d'aquest usuari" -_fileViewer: - title: "Detall del fitxer" - type: "Tipus de fitxer" - size: "Mida" - url: "URL" - uploadedAt: "Pujat el" - attachedNotes: "Notes amb aquest fitxer" - thisPageCanBeSeenFromTheAuthor: "Aquesta pàgina només la pot veure l'usuari que ha pujat aquest fitxer." -_externalResourceInstaller: - title: "Instal·lar des d'un lloc extern" - checkVendorBeforeInstall: "Assegura't que qui distribueix aquest recurs és fiable abans d'instal·lar-ho." - _plugin: - title: "Vols instal·lar aquest afegit?" - metaTitle: "Informació de l'afegit " - _theme: - title: "Vols instal·lar aquest tema?" - metaTitle: "Informació del tema" - _meta: - base: "Paleta de colors base" - _vendorInfo: - title: "Informació del distribuïdor " - endpoint: "Punt final referenciat" - hashVerify: "Verificació d'integritat " - _errors: - _invalidParams: - title: "Paràmetres no vàlids " -_reversi: - total: "Total" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 7fc72bdded..55f4d19432 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -366,8 +366,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Aktivovat hCaptchu" hcaptchaSiteKey: "Klíč stránky" hcaptchaSecretKey: "Tajný Klíč (Secret Key)" -mcaptchaSiteKey: "Klíč stránky" -mcaptchaSecretKey: "Tajný Klíč (Secret Key)" recaptcha: "reCAPTCHA" enableRecaptcha: "Zapnout ReCAPTCHu" recaptchaSiteKey: "Klíč stránky" @@ -1005,7 +1003,6 @@ resetPasswordConfirm: "Opravdu chcete resetovat heslo?" sensitiveWords: "Citlivá slova" sensitiveWordsDescription: "Viditelnost všech poznámek obsahujících některé z nakonfigurovaných slov bude automaticky nastavena na \"Domů\". Můžete jich uvést více tak, že je oddělíte pomocí řádků." sensitiveWordsDescription2: "Použití mezer vytvoří výrazy AND a obklopení klíčových slov lomítky je změní na regulární výraz." -prohibitedWordsDescription2: "Použití mezer vytvoří výrazy AND a obklopení klíčových slov lomítky je změní na regulární výraz." notesSearchNotAvailable: "Vyhledávání poznámek je nedostupné." license: "Licence" unfavoriteConfirm: "Opravdu chcete odstranit z oblíbených?" @@ -1095,14 +1092,7 @@ iHaveReadXCarefullyAndAgree: "Přečetl jsem si text \"{x}\" a souhlasím s ním icon: "Avatar" replies: "Odpovědět" renotes: "Přeposlat" -sourceCode: "Zdrojový kód" flip: "Otočit" -lastNDays: "Posledních {n} dnů" -surrender: "Zrušit" -_delivery: - stop: "Suspendováno" - _type: - none: "Publikuji" _initialAccountSetting: accountCreated: "Váš účet byl úspěšně vytvořen!" letsStartAccountSetup: "Pro začátek si nastavte svůj profil." @@ -1192,7 +1182,7 @@ _achievements: _login3: title: "Začátečník I" description: "Přihlaste se celkově za 3 dny" - flavor: "Ode dneška mi říkejte Cherrypikista." + flavor: "Ode dneška mi říkejte Misskista." _login7: title: "Začátečník II" description: "Přihlaste se celkově za 7 dní" @@ -1297,7 +1287,7 @@ _achievements: _viewAchievements3min: title: "Máš rád úspěchy" description: "Koukejte na váš seznam úspěchů alespoň po dobu 3 minut" - _iLoveCherryPick: + _iLoveMisskey: title: "Miluju CherryPick" description: "Zveřejněte \" I ❤ #CherryPick\"" flavor: "Vývojový tým CherryPick si velmi váží vaší podpory!" @@ -1650,6 +1640,8 @@ _sfx: notification: "Oznámení" chat: "Zprávy" chatBg: "Chat (Pozadí)" + antenna: "Antény" + channel: "Oznámení kanálu" _ago: future: "Budoucí" justNow: "Teď" @@ -1671,6 +1663,7 @@ _2fa: registerTOTP: "Registrovat aplikaci autentizátoru" step1: "Nejprve si do zařízení nainstalujte aplikaci pro ověřování (například {a} nebo {b})." step2: "Poté naskenujte QR kód zobrazený na této obrazovce." + step2Click: "Kliknutím na tento QR kód můžete zaregistrovat 2FA do bezpečnostního klíče nebo aplikace autentizace telefonu." step3Title: "Zadejte ověřovací kód" step3: "Pro dokončení nastavení zadejte token poskytnutý vaší aplikací." step4: "Od této chvíle budou všechny budoucí pokusy o přihlášení vyžadovat tento přihlašovací token." @@ -1724,7 +1717,7 @@ _auth: shareAccessTitle: "Udělovat oprávnění k aplikacím" shareAccess: "Chcete autorizovat \"{name}\" pro přístup k tomuto účtu?" shareAccessAsk: "Opravdu chcete této aplikaci povolit přístup k vašemu účtu?" - permission: "{name} požaduje tato oprávnění" + permission: "{jméno} požaduje tato oprávnění" permissionAsk: "Tato aplikace požaduje následující oprávnění" pleaseGoBack: "Vraťte se prosím zpět do aplikace" callback: "Návrat k aplikaci" @@ -1837,7 +1830,6 @@ _profile: _exportOrImport: allNotes: "Všechny poznámky" favoritedNotes: "Oblíbené poznámky" - clips: "Oříznout" followingList: "Sledovaní" muteList: "Ztlumit" blockingList: "Zablokovat" @@ -1948,7 +1940,7 @@ _notification: youGotMention: "{name} vás zmínil" youGotReply: "{name} vám odpověděl" youGotQuote: "{name} vás citoval" - youRenoted: "Poznámka od {name}" + youRenoted: "Poznámka od {jméno}" youWereFollowed: "Máte nového následovníka" youReceivedFollowRequest: "Obdrželi jste žádost o sledování" yourFollowRequestAccepted: "Vaše žádost o sledování byla přijata" @@ -2015,6 +2007,7 @@ _webhookSettings: createWebhook: "Vytvořit Webhook" name: "Jméno" secret: "Tajné" + events: "Události Webhook" active: "Zapnuto" _events: follow: "Při sledování uživatele" @@ -2024,13 +2017,7 @@ _webhookSettings: renote: "Při renotaci poznámky" reaction: "Při obdržení reakce" mention: "Při zmínce" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" _moderationLogTypes: suspend: "Zmrazit" resetPassword: "Resetovat heslo" createInvitation: "Vygenerovat pozvánku" -_reversi: - total: "Celkem" diff --git a/locales/da-DK.yml b/locales/da-DK.yml index 2589eb4b41..08c15ed092 100644 --- a/locales/da-DK.yml +++ b/locales/da-DK.yml @@ -1,4 +1,2 @@ --- _lang_: "Dansk" -headlineMisskey: "" -introMisskey: "ようこそ!CherryPickは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 32cfe3614c..a50e399ffc 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -121,15 +121,9 @@ sensitive: "Sensibel" add: "Hinzufügen" reaction: "Reaktionen" reactions: "Reaktionen" -emojiPicker: "Emoji auswählen" -pinnedEmojisForReactionSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie beim Reagieren als Erstes anzuzeigen." -pinnedEmojisSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie in der Emoji-Auswahl als Erstes anzuzeigen" -overwriteFromPinnedEmojisForReaction: "Überschreiben mit den Reaktions-Einstellungen" -overwriteFromPinnedEmojis: "Überschreiben mit den allgemeinen Einstellungen" reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen" rememberNoteVisibility: "Notizsichtbarkeit merken" attachCancel: "Anhang entfernen" -deleteFile: "Datei gelöscht" markAsSensitive: "Als sensibel markieren" unmarkAsSensitive: "Als nicht sensibel markieren" enterFileName: "Dateinamen eingeben" @@ -184,7 +178,7 @@ searchWith: "Suchen: {q}" youHaveNoLists: "Du hast keine Listen" followConfirm: "Möchtest du {name} wirklich folgen?" proxyAccount: "Proxy-Benutzerkonto" -proxyAccountDescription: "Ein Proxy-Konto ist ein Benutzerkonto, das unter bestimmten Bedingungen als Follower für Benutzer fremder Instanzen fungiert. Wenn zum Beispiel ein Benutzer einen Benutzer einer fremden Instanz zu einer Liste hinzufügt, werden die Aktivitäten des entfernten Benutzers nicht an die Instanz übermittelt, wenn kein lokaler Benutzer diesem Benutzer folgt; stattdessen folgt das Proxy-Konto." +proxyAccountDescription: "Ein Proxy-Benutzerkonto ist ein Benutzerkonto, das sich für Nutzer unter bestimmten Konditionen wie ein Follower aus einer fremden Instanz verhält. Zum Beispiel wird die Aktivität eines Nutzers aus einer fremden Instanz nicht an diese Instanz übermittelt, falls es keinen Benutzer dieser Instanz gibt, der diesem Nutzer aus fremder Instanz folgt. In diesem Fall folgt stattdessen das Proxy-Benutzerkonto." host: "Hostname" selectUser: "Benutzer auswählen" recipient: "Empfänger" @@ -266,7 +260,6 @@ removed: "Erfolgreich gelöscht" removeAreYouSure: "Möchtest du „{x}“ wirklich entfernen?" deleteAreYouSure: "Möchtest du „{x}“ wirklich löschen?" resetAreYouSure: "Wirklich zurücksetzen?" -areYouSure: "Bist du sicher?" saved: "Erfolgreich gespeichert" messaging: "Chat" upload: "Hochladen" @@ -361,7 +354,7 @@ enableLocalTimeline: "Lokale Chronik aktivieren" enableGlobalTimeline: "Globale Chronik aktivieren" disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind." registration: "Registrieren" -enableRegistration: "Registrierung neuer Benutzer erlauben" +enableRegistration: "Registration neuer Benutzer erlauben" invite: "Einladen" driveCapacityPerLocalAccount: "Drive-Kapazität pro lokalem Benutzerkonto" driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer fremder Instanzen" @@ -379,11 +372,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptcha aktivieren" hcaptchaSiteKey: "Site key" hcaptchaSecretKey: "Secret key" -mcaptcha: "mCaptcha" -enableMcaptcha: "mCaptcha aktivieren" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret key" -mcaptchaInstanceUrl: "mCaptcha Instanz-URL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA aktivieren" recaptchaSiteKey: "Site key" @@ -441,7 +429,7 @@ lastUsed: "Zuletzt benutzt" lastUsedAt: "Zuletzt verwendet: {t}" unregister: "Deaktivieren" passwordLessLogin: "Passwortloses Anmelden" -passwordLessLoginDescription: "Ermöglicht passwortloses Einloggen mit einem Security-Token oder Passkey" +passwordLessLoginDescription: "Ermöglicht passwortfreies Einloggen, nur via Security-Token oder Passkey" resetPassword: "Passwort zurücksetzen" newPasswordIs: "Das neue Passwort ist „{password}“" reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren" @@ -643,7 +631,6 @@ medium: "Mittel" small: "Klein" generateAccessToken: "Zugriffstoken generieren" permission: "Berechtigungen" -adminPermission: "Administratorberechtigung" enableAll: "Alle aktivieren" disableAll: "Alle deaktivieren" tokenRequested: "Zugriff zum Benutzerkonto gewähren" @@ -666,7 +653,7 @@ smtpSecureInfo: "Schalte dies aus, falls du STARTTLS verwendest." testEmail: "Emailversand testen" wordMute: "Wortstummschaltung" regexpError: "Fehler in einem regulären Ausdruck" -regexpErrorDescription: "Im regulären Ausdruck deiner in Zeile {line} von {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:" +regexpErrorDescription: "Im regulären Ausdruck deiner {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:" instanceMute: "Instanzstummschaltungen" userSaysSomething: "{name} hat etwas gesagt" makeActive: "Aktivieren" @@ -1050,7 +1037,6 @@ resetPasswordConfirm: "Wirklich Passwort zurücksetzen?" sensitiveWords: "Sensible Wörter" sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden." sensitiveWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden." -prohibitedWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden." hiddenTags: "Ausgeblendete Hashtags" hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden." notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar." @@ -1172,7 +1158,6 @@ hideRepliesToOthersInTimelineAll: "Antworten von allen momentan gefolgten Benutz confirmShowRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern in der Chronik anzeigen?" confirmHideRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern nicht in der Chronik anzeigen?" externalServices: "Externe Dienste" -sourceCode: "Quellcode" 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." @@ -1194,15 +1179,6 @@ signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen. cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden." doReaction: "Reagieren" code: "Code" -decorate: "Dekorieren" -addMfmFunction: "MFM hinzufügen" -sfx: "Soundeffekte" -lastNDays: "Letzten {n} Tage" -surrender: "Abbrechen" -_delivery: - stop: "Gesperrt" - _type: - none: "Wird veröffentlicht" _announcement: forExistingUsers: "Nur für existierende Nutzer" forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." @@ -1212,7 +1188,6 @@ _announcement: tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren." readConfirmTitle: "Als gelesen markieren?" readConfirmText: "Dies markiert den Inhalt von \"{title}\" als gelesen." - shouldNotBeUsedToPresentPermanentInfo: "Es wird empfohlen, Ankündigungen für aktuelle und zeitlich begrenzte Neuigkeiten zu nutzen, statt für Informationen, die langfristig relevant sind." dialogAnnouncementUxWarn: "Bei der Verwendung von mehr als zwei Meldungen im Dialog-Format wird um Vorsicht geboten, da dies negative Auswirkungen auf die UX haben kann." silence: "Keine Benachrichtigung" silenceDescription: "Wenn aktiviert, gibt diese Meldung keine Nachricht aus und muss nicht als \"gelesen\" markiert werden." @@ -1242,24 +1217,6 @@ _initialTutorial: description: "Hier kannst du sehen, wie CherryPick funktioniert" _note: title: "Was sind Notizen?" - description: "Beiträge auf CherryPick heißen \"Notizen\". Notizen werden chronologisch in der Chronik angeordnet und in Echtzeit aktualisiert." - reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen." - _reaction: - title: "Was sind Reaktionen?" - reactToContinue: "Füge eine Reaktion hinzu, um fortzufahren." - reactNotification: "Du erhältst Echtzeit-Benachrichtigungen, wenn jemand auf deine Notiz reagiert." - _postNote: - _visibility: - description: "Du kannst einschränken, wer deine Notiz sehen kann." - public: "Deine Notiz wird für alle Nutzer sichtbar sein." - doNotSendConfidencialOnDirect1: "Sei vorsichtig, wenn du sensible Informationen verschickst!" - _cw: - title: "Inhaltswarnung" - _done: - title: "Du hast das Tutorial abgeschlossen! 🎉" -_timelineDescription: - local: "In der lokalen Chronik siehst du Notizen von allen Benutzern auf diesem Server." - global: "In der globalen Chronik siehst du Notizen von allen föderierten Servern." _serverRules: description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen." _serverSettings: @@ -1272,8 +1229,6 @@ _serverSettings: shortName: "Abkürzung" shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist." 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. " _accountMigration: moveFrom: "Von einem anderen Konto zu diesem migrieren" moveFromSub: "Alias für ein anderes Konto erstellen" @@ -1345,7 +1300,7 @@ _achievements: _login3: title: "Anfänger Ⅰ" description: "An 3 Tagen eingeloggt" - flavor: "Nenn' mich ab heute Cherrypikist" + flavor: "Nenn' mich ab heute Misskist" _login7: title: "Anfänger Ⅱ" description: "An 7 Tagen eingeloggt" @@ -1534,8 +1489,6 @@ _achievements: _smashTestNotificationButton: title: "Testüberfluss" description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne" - _tutorialCompleted: - description: "Tutorial abgeschlossen" _role: new: "Rolle erstellen" edit: "Rolle bearbeiten" @@ -1546,9 +1499,7 @@ _role: assignTarget: "Zuweisungsart" descriptionOfAssignTarget: "Manuell bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\nKonditional bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird." manual: "Manuell" - manualRoles: "Manuelle Rollen" conditional: "Konditional" - conditionalRoles: "Bedingte Rolle" condition: "Bedingung" isConditionalRole: "Dies ist eine konditionale Rolle." isPublic: "Öffentliche Rolle" @@ -1591,14 +1542,13 @@ _role: webhookMax: "Maximale Anzahl an Webhooks" clipMax: "Maximale Anzahl an Clips" noteEachClipsMax: "Maximale Anzahl an Notizen innerhalb eines Clips" - userListMax: "Maximale Anzahl an Benutzerlisten" - userEachUserListsMax: "Maximale Anzahl an Benutzern in einer Benutzerliste" + userListMax: "Maximale Anzahl an Benutzern in einer Benutzerliste" + userEachUserListsMax: "Maximale Anzahl an Benutzerlisten" rateLimitFactor: "Versuchsanzahl" descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." canHideAds: "Kann Werbung ausblenden" canSearchNotes: "Nutzung der Notizsuchfunktion" canUseTranslator: "Verwendung des Übersetzers" - avatarDecorationLimit: "Maximale Anzahl an Profilbilddekorationen, die angebracht werden können" _condition: isLocal: "Lokaler Benutzer" isRemote: "Benutzer fremder Instanz" @@ -1627,7 +1577,6 @@ _emailUnavailable: disposable: "Wegwerf-Email-Adressen können nicht verwendet werden" mx: "Dieser Email-Server ist ungültig" smtp: "Dieser Email-Server antwortet nicht" - banned: "Du kannst dich mit dieser E-Mail-Adresse nicht registrieren" _ffVisibility: public: "Öffentlich" followers: "Nur für Follower sichtbar" @@ -1886,6 +1835,8 @@ _sfx: notification: "Benachrichtigungen" chat: "Chat" chatBg: "Chat (Hintergrund)" + antenna: "Antennen" + channel: "Kanalbenachrichtigung" _ago: future: "Zukunft" justNow: "Gerade eben" @@ -1907,6 +1858,7 @@ _2fa: registerTOTP: "Authentifizierungs-App registrieren" step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." + step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren." step2Uri: "Nutzt du ein Desktopprogramm, gib folgende URI eingeben" step3Title: "Authentifizierungsscode eingeben" step3: "Gib zum Abschluss den Code (Token) ein, der von deiner App angezeigt wird." @@ -2022,7 +1974,6 @@ _widgets: _userList: chooseList: "Liste auswählen" clicker: "Klickzähler" - birthdayFollowings: "Nutzer, die heute Geburtstag haben" _cw: hide: "Inhalt verbergen" show: "Inhalt anzeigen" @@ -2088,7 +2039,6 @@ _profile: _exportOrImport: allNotes: "Alle Notizen" favoritedNotes: "Als Favorit markierte Notizen" - clips: "Clip erstellen" followingList: "Gefolgte Benutzer" muteList: "Stummschaltungen" blockingList: "Blockierungen" @@ -2281,6 +2231,7 @@ _webhookSettings: createWebhook: "Webhook erstellen" name: "Name" secret: "Secret" + events: "Webhook-Ereignisse" active: "Aktiviert" _events: follow: "Wenn du jemandem folgst" @@ -2290,10 +2241,6 @@ _webhookSettings: renote: "Wenn du ein Renote erhältst" reaction: "Wenn du eine Reaktion erhältst" mention: "Wenn du erwähnt wirst" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" _moderationLogTypes: createRole: "Rolle erstellt" deleteRole: "Rolle gelöscht" @@ -2377,9 +2324,3 @@ _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." -_reversi: - blackOrWhite: "Schwarz/Weiß" - rules: "Regeln" - black: "Schwarz" - white: "Weiß" - total: "Gesamt" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 0fc9488f1d..6a285118de 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -303,6 +303,8 @@ _sfx: notification: "Ειδοποιήσεις" chat: "Συνομιλία" chatBg: "Συνομιλία (Παρασκήνιο)" + antenna: "Αντένες" + channel: "Ειδοποιήσεις καναλιών" _ago: future: "Μελλοντικό" justNow: "Μόλις τώρα" @@ -356,7 +358,6 @@ _profile: username: "Όνομα μέλους" _exportOrImport: allNotes: "Όλα τα σημειώματα" - clips: "Κλιπ" followingList: "Ακολουθεί" muteList: "Μέλη σε σίγαση" blockingList: "Μπλοκαρισμένα μέλη" @@ -396,6 +397,3 @@ _webhookSettings: name: "Όνομα" _moderationLogTypes: suspend: "Αποβολή" -_reversi: - total: "Σύνολο" - diff --git a/locales/en-US.yml b/locales/en-US.yml index e019b6ee02..1746363f75 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1,6 +1,5 @@ --- _lang_: "English" -forceRenoteVisibilitySelector: "Specify visibility of renote" cherrypickLabs: "CherryPick Labs" cherrypickLabsDescription: "Why not try some of the features that are still under development? Some features may not work properly." copiedLink: "The link has been copied!" @@ -137,7 +136,6 @@ copyFileId: "Copy file ID" copyFolderId: "Copy folder ID" copyProfileUrl: "Copy profile URL" searchUser: "Search for a user" -searchThisUsersNotes: "Search this user’s notes" reply: "Reply" loadMore: "Load more" showMore: "Show more" @@ -186,7 +184,6 @@ enterEmoji: "Enter an emoji" renote: "Renote" unrenote: "Remove renote" renoted: "Renoted." -renotedToX: "Renote to {name}." quoted: "Quoted." replied: "Replied." cantRenote: "This post can't be renoted." @@ -194,8 +191,6 @@ cantReRenote: "A renote can't be renoted." quote: "Quote" inChannelRenote: "Channel-only Renote" inChannelQuote: "Channel-only Quote" -renoteToChannel: "Renote to channel" -renoteToOtherChannel: "Renote to other channel" pinnedNote: "Pinned note" pinned: "Pin to profile" you: "You" @@ -206,15 +201,10 @@ add: "Add" reaction: "Reactions" reactions: "Reactions" emojiPicker: "Emoji picker" -pinnedEmojisForReactionSettingDescription: "Set the emojis to be pinned and displayed when reacting." -pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker." emojiPickerDisplay: "Emoji picker display" -overwriteFromPinnedEmojisForReaction: "Override from reaction settings" -overwriteFromPinnedEmojis: "Override from general settings" reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add." rememberNoteVisibility: "Remember note visibility settings" attachCancel: "Remove attachment" -deleteFile: "Delete file" markAsSensitive: "Mark as sensitive" unmarkAsSensitive: "Unmark as sensitive" enterFileName: "Enter filename" @@ -226,20 +216,15 @@ block: "Block" unblock: "Unblock" suspend: "Suspend" unsuspend: "Unsuspend" -setAsSensitive: "Set as sensitive user" -unsetAsSensitive: "Unset as sensitive user" blockConfirm: "Are you sure that you want to block this account?" unblockConfirm: "Are you sure that you want to unblock this account?" suspendConfirm: "Are you sure that you want to suspend this account?" unsuspendConfirm: "Are you sure that you want to unsuspend this account?" -setSensitiveConfirm: "Are you sure that you want to set this account as sensitive?" -unsetSensitiveConfirm: "Are you sure that you want to unset this account as sensitive?" selectList: "Select a list" editList: "Edit list" selectChannel: "Select a channel" selectAntenna: "Select an antenna" editAntenna: "Edit antenna" -createAntenna: "Create an antenna" selectWidget: "Select a widget" editWidgets: "Edit widgets" editWidgetsExit: "Done" @@ -251,7 +236,7 @@ emojiUrl: "Emoji URL" addEmoji: "Add an emoji" settingGuide: "Recommended settings" cacheRemoteFiles: "Cache remote files" -cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote servers. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." +cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." youCanCleanRemoteFilesCache: "You can clear the cache by clicking the 🗑️ button in the file management view." cacheRemoteSensitiveFiles: "Cache sensitive remote files" cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching." @@ -262,16 +247,10 @@ flagAsCatDescription: "Enable this option to mark this account as a cat." flagShowTimelineReplies: "Show replies in timeline" flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on." autoAcceptFollowed: "Automatically approve follow requests from users you're following" -carefulBot: "Require follow requests from bots" -carefulBotDescription: "If this option is enabled, you will need to approve follow requests from bots." addAccount: "Add account" reloadAccountsList: "Reload account list" loginFailed: "Failed to sign in" showOnRemote: "View on remote instance" -continueOnRemote: "リモートで続行" -chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub" -specifyServerHost: "Specify a server host directly" -inputHostName: "Enter the domain" general: "General" wallpaper: "Wallpaper" setWallpaper: "Set wallpaper" @@ -282,7 +261,6 @@ followConfirm: "Are you sure that you want to follow {name}?" proxyAccount: "Proxy account" proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead." host: "Host" -selectSelf: "Select myself" selectUser: "Select a user" recipient: "Recipient" annotation: "Comments" @@ -298,7 +276,6 @@ perDay: "Per Day" stopActivityDelivery: "Stop sending activities" blockThisInstance: "Block this instance" silenceThisInstance: "Silence this instance" -mediaSilenceThisInstance: "Media-silence this server" operations: "Operations" software: "Software" version: "Version" @@ -319,9 +296,7 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote blockedInstances: "Blocked Instances" blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance." silencedInstances: "Silenced instances" -silencedInstancesDescription: "List the host names of the servers that you want to silence, separated by a new line. All accounts belonging to the listed servers 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 servers." -mediaSilencedInstances: "Media-silenced servers" -mediaSilencedInstancesDescription: "List the host names of the servers that you want to media-silence, separated by a new line. All accounts belonging to the listed servers will be treated as sensitive, and can't use custom emojis. This will not affect the blocked servers." +silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances." muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" @@ -392,7 +367,6 @@ termsOfService: "Terms of Service" start: "Begin" home: "Home" remoteUserCaution: "As this user is from a remote instance, the shown information may be incomplete." -deprecatedCaution: "This feature is deprecated and will remove in the future. \n If you needed, please take a backup." activity: "Activity" images: "Images" image: "Image" @@ -414,7 +388,6 @@ selectFile: "Select a file" selectFiles: "Select files" selectFolder: "Select a folder" selectFolders: "Select folders" -fileNotSelected: "" renameFile: "Rename file" folderName: "Folder name" createFolder: "Create a folder" @@ -482,11 +455,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Enable hCaptcha" hcaptchaSiteKey: "Site key" hcaptchaSecretKey: "Secret key" -mcaptcha: "mCaptcha" -enableMcaptcha: "Enable mCaptcha" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret key" -mcaptchaInstanceUrl: "mCaptcha server URL" recaptcha: "reCAPTCHA" enableRecaptcha: "Enable reCAPTCHA" recaptchaSiteKey: "Site key" @@ -502,7 +470,6 @@ name: "Name" antennaSource: "Antenna source" antennaKeywords: "Keywords to listen to" antennaExcludeKeywords: "Keywords to exclude" -antennaExcludeBots: "Exclude bot accounts" antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." notifyAntenna: "Notify about new notes" withFileAntenna: "Only notes with files" @@ -579,12 +546,10 @@ noteOf: "Note by {user}" inviteToGroup: "Invite to group" quoteAttached: "Quote" quoteQuestion: "Append as quote?" -attachAsFileQuestion: "The text in clipboard is long. Would you want to attach it as text file?" noMessagesYet: "No messages yet" newMessageExists: "There are new messages" onlyOneFileCanBeAttached: "You can only attach one file to a message" signinRequired: "Please register or sign in before continuing" -signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server." invitations: "Invites" invitationCode: "Invitation code" checking: "Checking..." @@ -611,7 +576,6 @@ disableDrawer: "Don't use drawer-style menus" youHaveNoGroups: "You have no groups" joinOrCreateGroup: "Get invited to a group or create your own." showNoteActionsOnlyHover: "Only show note actions on hover" -showReactionsCount: "See the number of reactions in notes" noHistory: "No history available" signinHistory: "Login history" enableAdvancedMfm: "Enable advanced MFM" @@ -757,7 +721,6 @@ medium: "Medium" small: "Small" generateAccessToken: "Generate access token" permission: "Permissions" -adminPermission: "Admin Permissions" enableAll: "Enable all" disableAll: "Disable all" tokenRequested: "Grant access to account" @@ -774,7 +737,7 @@ smtpHost: "Host" smtpPort: "Port" smtpUser: "Username" smtpPass: "Password" -emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP authentication" +emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP verification" smtpSecure: "Use implicit SSL/TLS for SMTP connections" smtpSecureInfo: "Turn this off when using STARTTLS" testEmail: "Test email delivery" @@ -796,13 +759,11 @@ channel: "Channels" create: "Create" notificationSetting: "Notification settings" notificationSettingDesc: "Select the types of notification to display." -notificationFlush: "Flush ALL Notifications" useGlobalSetting: "Use global settings" useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made." other: "Other" regenerateLoginToken: "Regenerate login token" regenerateLoginTokenDescription: "Regenerates the token used internally during login. Normally this action is not necessary. If regenerated, all devices will be logged out." -theKeywordWhenSearchingForCustomEmoji: "This is the keyword when searching for custom emojis." setMultipleBySeparatingWithSpace: "Separate multiple entries with spaces." fileIdOrUrl: "File ID or URL" behavior: "Behavior" @@ -958,7 +919,6 @@ administration: "Management" accounts: "Accounts" switch: "Switch" noMaintainerInformationWarning: "Maintainer information is not configured." -noInquiryUrlWarning: "Inquiry URL isn’t set" noBotProtectionWarning: "Bot protection is not configured." configure: "Configure" postToGallery: "Create new gallery post" @@ -1123,7 +1083,6 @@ neverShow: "Don't show again" remindMeLater: "Maybe later" didYouLikeMisskey: "Have you taken a liking to CherryPick?" pleaseDonate: "{host} uses the free software, CherryPick. We would highly appreciate your donations so development of CherryPick can continue!" -correspondingSourceIsAvailable: "The corresponding source code is available at {anchor}" roles: "Roles" role: "Role" noRole: "Role not found" @@ -1151,7 +1110,6 @@ thisPostMayBeAnnoyingHome: "Post to home timeline" thisPostMayBeAnnoyingCancel: "Cancel" thisPostMayBeAnnoyingIgnore: "Post anyway" collapseRenotes: "Collapse renotes you've already seen" -collapseRenotesDescription: "Collapse notes that you've reacted to or renoted before." collapseDefault: "Collapse notes using specific MFM syntax" internalServerError: "Internal Server Error" internalServerErrorDescription: "The server has run into an unexpected error." @@ -1176,9 +1134,6 @@ resetPasswordConfirm: "Really reset your password?" sensitiveWords: "Sensitive words" sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks." sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." -prohibitedWords: "Prohibited words" -prohibitedWordsDescription: "Enables an error when attempting to post a note containing the set word(s). Multiple words can be set, separated by a new line." -prohibitedWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." hiddenTags: "Hidden hashtags" hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines." notesSearchNotAvailable: "Note search is unavailable." @@ -1197,8 +1152,6 @@ limitWidthOfReaction: "Limits the maximum width of reactions and display them in noteIdOrUrl: "Note ID or URL" video: "Video" videos: "Videos" -audio: "Audio" -audioFiles: "Audio" dataSaver: "Data Saver" accountMigration: "Account Migration" accountMoved: "This user has moved to a new account:" @@ -1229,8 +1182,6 @@ preservedUsernames: "Reserved usernames" preservedUsernamesDescription: "List usernames to reserve separated by linebreaks. These will become unable during normal account creation, but can be used by administrators to manually create accounts. Already existing accounts using these usernames will not be affected." createNoteFromTheFile: "Compose note from this file" archive: "Archive" -archived: "Archived" -unarchive: "Unarchive" channelArchiveConfirmTitle: "Really archive {name}?" channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore." thisChannelArchived: "This channel has been archived." @@ -1241,9 +1192,6 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)" preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored." options: "Options" specifyUser: "Specific user" -lookupConfirm: "Do you want to look up?" -openTagPageConfirm: "Do you want to open a hashtag page?" -specifyHost: "Specify a host" failedToPreviewUrl: "Could not preview" update: "Update" rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction" @@ -1305,7 +1253,6 @@ showRenotes: "Show renotes" edited: "Edited" notificationRecieveConfig: "Notification Settings" mutualFollow: "Mutual follow" -followingOrFollower: "Following or follower" fileAttachedOnly: "Only notes with files" showRepliesToOthersInTimeline: "Show replies to others in timeline" hideRepliesToOthersInTimeline: "Hide replies to others from timeline" @@ -1314,24 +1261,15 @@ hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you foll confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?" confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?" externalServices: "External Services" -sourceCode: "Source code" -sourceCodeIsNotYetProvided: "Source code is not yet available. Contact the administrator to fix this problem." -repositoryUrl: "Repository URL" -repositoryUrlDescription: "If you are using CherryPick as is (without any changes to the source code), enter https://github.com/kokonect-link/cherrypick" -repositoryUrlOrTarballRequired: "If you have not published a repository, you must provide a tarball instead. See .config/example.yml for more information." -feedback: "Feedback" -feedbackUrl: "Feedback URL" impressum: "Impressum" impressumUrl: "Impressum URL" impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites." privacyPolicy: "Privacy Policy" privacyPolicyUrl: "Privacy Policy URL" tosAndPrivacyPolicy: "Terms of Service and Privacy Policy" -statusUrl: "Servis Status URL" avatarDecorations: "Avatar decorations" attach: "Attach" detach: "Remove" -detachAll: "Remove All" angle: "Angle" flip: "Flip" showAvatarDecorations: "Show avatar decorations" @@ -1345,52 +1283,13 @@ cwNotationRequired: "If \"Hide content\" is enabled, a description must be provi doReaction: "Add reaction" code: "Code" reloadRequiredToApplySettings: "Reloading is required to apply the settings." -remainingN: "Remaining: {n}" -overwriteContentConfirm: "Are you sure you want to overwrite the current content?" -seasonalScreenEffect: "Seasonal Screen Effect" decorate: "Decorate" -addMfmFunction: "Add MFM" -enableQuickAddMfmFunction: "Show advanced MFM picker" -bubbleGame: "Bubble Game" -sfx: "Sound Effects" -soundWillBePlayed: "Sound will be played" -showReplay: "View Replay" -replay: "Replay" -replaying: "Showing replay" -endReplay: "Exit Replay" -copyReplayData: "Copy replay data" -ranking: "Ranking" -lastNDays: "Last {n} days" -backToTitle: "Go back to title" -hemisphere: "Where are you located" -withSensitive: "Include notes with sensitive files" -userSaysSomethingSensitive: "Post by {name} contains sensitive content" -enableHorizontalSwipe: "Swipe to switch tabs" -loading: "Loading" -surrender: "Cancel" -gameRetry: "Retry" -notUsePleaseLeaveBlank: "Leave blank if not used" -useTotp: "Enter the One-Time Password" -useBackupCode: "Use the backup codes" -launchApp: "Launch the app" -useNativeUIForVideoAudioPlayer: "Use UI of browser when play video and audio" -keepOriginalFilename: "Keep original file name" -keepOriginalFilenameDescription: "If you turn off this setting, files names will be replaced with random string automatically when you upload files." -noDescription: "There is not the explanation" -alwaysConfirmFollow: "Always confirm when following" -inquiry: "Contact" -tryAgain: "Please try again later" -confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media" -sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?" -createdLists: "Created lists" -createdAntennas: "Created antennas" showUnreadNotificationsCount: "Show the number of unread notifications" showCatOnly: "Show only cats" additionalPermissionsForFlash: "Allow to add permission to Play" thisFlashRequiresTheFollowingPermissions: "This Play requires the following permissions" doYouWantToAllowThisPlayToAccessYourAccount: "Do you want to allow this Play to access your account?" translateProfile: "Translate profile" -getQrCode: "Get QR code" _nsfwOpenBehavior: click: "Click to open" doubleClick: "Double click to open" @@ -1407,30 +1306,6 @@ _showingAnimatedImages: inactive: "Stop after a certain amount of time" _messaging: direct: "Direct Message" -_delivery: - status: "Delivery status" - stop: "Suspended" - resume: "Delivery resume" - _type: - none: "Publishing" - manuallySuspended: "Manually suspended" - goneSuspended: "Server is suspended due to server deletion" - autoSuspendedForNotResponding: "Server is suspended due to no responding" -_bubbleGame: - howToPlay: "How to play" - hold: "Hold" - _score: - score: "Score" - scoreYen: "Amount of money earned" - highScore: "High score" - maxChain: "Maximum number of chains" - yen: "{yen} Yen" - estimatedQty: "{qty} Pieces" - scoreSweets: "{onigiriQtyWithUnit} Onigiri" - _howToPlay: - section1: "Adjust the position and drop the object into the box." - section2: "When two objects of the same type touch each other, they will change into a different object and you score points." - section3: "The game is over when objects overflow from the box. Aim for a high score by fusing objects together while you avoid overflowing the box!" _announcement: forExistingUsers: "Existing users only" forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." @@ -1440,7 +1315,7 @@ _announcement: tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete." readConfirmTitle: "Mark as read?" readConfirmText: "This will mark the contents of \"{title}\" as read." - shouldNotBeUsedToPresentPermanentInfo: "It's best to use announcements to publish fresh and time-bound information, not for information that will be relevant in the long term." + shouldNotBeUsedToPresentPermanentInfo: "As it may significantly impact the user experience for new users, it is recommended to use notifications in the flow information rather than stock information." dialogAnnouncementUxWarn: "Having two or more dialog-style notifications simultaneously can significantly impact the user experience, so please use them carefully." silence: "No notification" silenceDescription: "Turning this on will skip the notification of this announcement and the user won't need to read it." @@ -1461,10 +1336,6 @@ _cherrypick: showRenoteConfirmPopupDescription: "This setting must have the \"General - Show renote and quote buttons separately\" setting turned on." expandOnNoteClick: "Open note on click" expandOnNoteClickDescription: "If disabled, you can still open 'Details' in the notes menu or by clicking the timestamp." - expandOnNoteClickBehavior: "When click to open a note" - _expandOnNoteClickBehavior: - click: "Click to open" - doubleClick: "Double-click to open" displayHeaderNavBarWhenScroll: "Show elements when scrolling (header, floating buttons, navigation bar)" _displayHeaderNavBarWhenScroll: all: "Display all" @@ -1622,8 +1493,6 @@ _serverSettings: fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability." fanoutTimelineDbFallback: "Fallback to database" fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved." - inquiryUrl: "Inquiry URL" - inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." _accountMigration: moveFrom: "Migrate another account to this one" moveFromSub: "Create alias to another account" @@ -1695,7 +1564,7 @@ _achievements: _login3: title: "Beginner I" description: "Log in for a total of 3 days" - flavor: "Starting today, just call me Cherrypikist" + flavor: "Starting today, just call me Misskist" _login7: title: "Beginner II" description: "Log in for a total of 7 days" @@ -1887,13 +1756,6 @@ _achievements: _tutorialCompleted: title: "CherryPick Elementary Course Diploma" description: "Tutorial completed" - _bubbleGameExplodingHead: - title: "🤯" - description: "The biggest object in the bubble game" - _bubbleGameDoubleExplodingHead: - title: "Double🤯" - description: "Two of the biggest objects in the bubble game at the same time" - flavor: "You can fill a lunch box like this 🤯 🤯 a bit." _role: new: "New role" edit: "Edit role" @@ -1935,7 +1797,6 @@ _role: ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" canEditNote: "Note editing" - mentionMax: "Maximum number of mentions in a note" canInvite: "Can create instance invite codes" inviteLimit: "Invite limit" inviteLimitCycle: "Invite limit cooldown" @@ -1944,7 +1805,6 @@ _role: canManageAvatarDecorations: "Manage avatar decorations" driveCapacity: "Drive capacity" alwaysMarkNsfw: "Always mark files as NSFW" - canUpdateBioMedia: "Allow to edit an icon or a banner image" pinMax: "Maximum number of pinned notes" antennaMax: "Maximum number of antennas" wordMuteMax: "Maximum number of characters allowed in word mutes" @@ -1958,16 +1818,9 @@ _role: canHideAds: "Can hide ads" canSearchNotes: "Usage of note search" canUseTranslator: "Translator usage" - avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" _condition: - roleAssignedTo: "Assigned to manual roles" isLocal: "Local user" isRemote: "Remote user" - isCat: "Cat Users" - isBot: "Bot Users" - isSuspended: "Suspended user" - isLocked: "Private accounts" - isExplorable: "Effective user of \"make an account discoverable\"" createdLessThan: "Less than X has passed since account creation" createdMoreThan: "More than X has passed since account creation" followersLessThanOrEq: "Has X or fewer followers" @@ -1997,7 +1850,6 @@ _emailUnavailable: disposable: "Disposable email addresses may not be used" mx: "This email server is invalid" smtp: "This email server is not responding" - banned: "You cannot register with this email address" _ffVisibility: public: "Public" followers: "Visible to followers only" @@ -2041,7 +1893,6 @@ _plugin: installWarn: "Please do not install untrustworthy plugins." manage: "Manage plugins" viewSource: "View source" - viewLog: "Show log" _preferencesBackups: list: "Created backups" saveNew: "Save new backup" @@ -2071,8 +1922,6 @@ _aboutMisskey: contributors: "Main contributors" allContributors: "All contributors" source: "Source code" - original: "Original" - thisIsModifiedVersion: "{name} uses a modified version of the original CherryPick." translation: "Translate Misskey" donate: "Donate to Misskey" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" @@ -2282,6 +2131,8 @@ _sfx: notification: "Notifications" chat: "Chat" chatBg: "Chat (Background)" + antenna: "Antennas" + channel: "Channel notifications" reaction: "On choosing a reaction" _soundSettings: driveFile: "Use an audio file in Drive." @@ -2290,7 +2141,6 @@ _soundSettings: driveFileTypeWarnDescription: "Select an audio file" driveFileDurationWarn: "The audio is too long." driveFileDurationWarnDescription: "Long audio may disrupt using CherryPick. Still continue?" - driveFileError: "It couldn't load the sound. Please change the setting." _ago: future: "Future" justNow: "Just now" @@ -2320,6 +2170,7 @@ _2fa: registerTOTP: "Register authenticator app" step1: "First, install an authentication app (such as {a} or {b}) on your device." step2: "Then, scan the QR code displayed on this screen." + step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app." step2Uri: "Enter the following URI if you are using a desktop program" step3Title: "Enter an authentication code" step3: "Enter the authentication code (token) provided by your app to finish setup." @@ -2343,7 +2194,6 @@ _2fa: backupCodesDescription: "You can use these codes to gain access to your account in case of becoming unable to use your two-factor authentificator app. Each can only be used once. Please keep them in a safe place." backupCodeUsedWarning: "A backup code has been used. Please reconfigure two-factor authentification as soon as possible if you are no longer able to use it." backupCodesExhaustedWarning: "All backup codes have been used. Should you lose access to your two-factor authentification app, you will be unable to access this account. Please reconfigure two-factor authentification." - moreDetailedGuideHere: "Here is detailed guide" _permissions: "read:account": "View your account information" "write:account": "Edit your account information" @@ -2381,54 +2231,6 @@ _permissions: "write:flash": "Edit Plays" "read:flash-likes": "View list of liked Plays" "write:flash-likes": "Edit list of liked Plays" - "read:admin:abuse-user-reports": "View user reports" - "write:admin:delete-account": "Delete user account" - "write:admin:delete-all-files-of-a-user": "Delete all files of a user" - "read:admin:index-stats": "View database index stats" - "read:admin:table-stats": "View database table stats" - "read:admin:user-ips": "View user IP addresses" - "read:admin:meta": "View instance metadata" - "write:admin:reset-password": "Reset user password" - "write:admin:resolve-abuse-user-report": "Resolve user report" - "write:admin:send-email": "Send email" - "read:admin:server-info": "View server info" - "read:admin:show-moderation-log": "View moderation log" - "read:admin:show-user": "View private user info" - "write:admin:suspend-user": "Suspend user" - "write:admin:unset-user-avatar": "Remove user avatar" - "write:admin:unset-user-banner": "Remove user banner" - "write:admin:unsuspend-user": "Unsuspend user" - "write:admin:meta": "Manage instance metadata" - "write:admin:user-note": "Manage moderation note" - "write:admin:roles": "Manage roles" - "read:admin:roles": "View roles" - "write:admin:relays": "Manage relays" - "read:admin:relays": "View relays" - "write:admin:invite-codes": "Manage invite codes" - "read:admin:invite-codes": "View invite codes" - "write:admin:announcements": "Manage announcements" - "read:admin:announcements": "View announcements" - "write:admin:avatar-decorations": "Can manage avatar decorations" - "read:admin:avatar-decorations": "View avatar decorations" - "write:admin:federation": "Manage federation data" - "write:admin:account": "Manage user account" - "read:admin:account": "View user account" - "write:admin:emoji": "Manage emoji" - "read:admin:emoji": "View emoji" - "write:admin:queue": "Manage job queue" - "read:admin:queue": "View job queue info" - "write:admin:promo": "Manage promotion notes" - "write:admin:drive": "Manage user drive" - "read:admin:drive": "View user drive info" - "read:admin:stream": "Use WebSocket API for Admin" - "write:admin:ad": "Manage ads" - "read:admin:ad": "View ads" - "write:invite-codes": "Create invite codes" - "read:invite-codes": "Get invite codes" - "write:clip-favorite": "Manage favorited clips" - "read:clip-favorite": "View favorited clips" - "read:federation": "Get federation data" - "write:report-abuse": "Report violation" _auth: shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" @@ -2485,9 +2287,6 @@ _widgets: chooseList: "Select a list" clicker: "Clicker" birthdayFollowings: "Users who celebrate their birthday today" - dice: "Dice" - - search: "Search" _cw: hide: "Hide" show: "Show content" @@ -2524,8 +2323,6 @@ _visibility: followersDescription: "Make visible to your followers only" specified: "Direct" specifiedDescription: "Make visible for specified users only" - private: "Private" - privateDescription: "Make visible for only yourself" disableFederation: "Defederate" disableFederationDescription: "Don't transmit to other instances" _postForm: @@ -2553,11 +2350,9 @@ _profile: changeAvatar: "Change avatar" changeBanner: "Change banner" verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field." - avatarDecorationMax: "You can add up to {max} decorations." _exportOrImport: allNotes: "All notes" favoritedNotes: "Favorite notes" - clips: "Clip" followingList: "Followed users" muteList: "Muted users" blockingList: "Blocked users" @@ -2594,7 +2389,6 @@ _instanceCharts: _timelines: home: "Home" local: "Local" - media: "Media" social: "Social" global: "Global" _play: @@ -2612,7 +2406,6 @@ _play: title: "Title" script: "Script" summary: "Description" - visibilityDescription: "Putting it private means it won't be visible on your profile, but anyone that has the URL can still access it." _pages: newPage: "Create a new Page" editPage: "Edit this Page" @@ -2657,8 +2450,6 @@ _pages: section: "Section" image: "Images" button: "Button" - dynamic: "Dynamic Blocks" - dynamicDescription: "This block has been abolished. Please use {play} from now on." note: "Embedded note" _note: id: "Note ID" @@ -2690,10 +2481,8 @@ _notification: sendTestNotification: "Send test notification" notificationWillBeDisplayedLikeThis: "Notifications look like this" reactedBySomeUsers: "{n} users reacted" - likedBySomeUsers: "{n} users liked your note" renotedBySomeUsers: "Renote from {n} users" followedBySomeUsers: "Followed by {n} users" - flushNotification: "Clear notifications" _types: all: "All" note: "New notes" @@ -2718,7 +2507,6 @@ _deck: alwaysShowMainColumn: "Always show main column" columnAlign: "Align columns" addColumn: "Add column" - newNoteNotificationSettings: "Notification setting for new notes" configureColumn: "Column settings" swapLeft: "Swap with the left column" swapRight: "Swap with the right column" @@ -2757,10 +2545,9 @@ _drivecleaner: orderByCreatedAtAsc: "Ascending Dates" _webhookSettings: createWebhook: "Create Webhook" - modifyWebhook: "Modify Webhook" name: "Name" secret: "Secret" - trigger: "Trigger" + events: "Webhook Events" active: "Enabled" _events: follow: "When following a user" @@ -2770,26 +2557,6 @@ _webhookSettings: renote: "When renoted" reaction: "When receiving a reaction" mention: "When being mentioned" - _systemEvents: - abuseReport: "When received a new abuse report" - abuseReportResolved: "When resolved abuse reports" - userCreated: "When user is created" - deleteConfirm: "Are you sure you want to delete the Webhook?" -_abuseReport: - _notificationRecipient: - createRecipient: "Add a recipient for abuse reports" - modifyRecipient: "Edit a recipient for abuse reports" - recipientType: "Notification type" - _recipientType: - mail: "Email" - webhook: "Webhook" - _captions: - mail: "Send the email to moderators' email addresses when you receive abuse." - webhook: "Send a notification to SystemWebhook when you receive or resolve abuse." - keywords: "Keywords" - notifiedUser: "Users to notify" - notifiedWebhook: "Webhook to use" - deleteConfirm: "Are you sure that you want to delete the notification recipient?" _moderationLogTypes: createRole: "Role created" deleteRole: "Role deleted" @@ -2814,7 +2581,6 @@ _moderationLogTypes: resetPassword: "Password reset" suspendRemoteInstance: "Remote instance suspended" unsuspendRemoteInstance: "Remote instance unsuspended" - updateRemoteInstanceNote: "Moderation note updated for remote instance." markSensitiveDriveFile: "File marked as sensitive" unmarkSensitiveDriveFile: "File unmarked as sensitive" resolveAbuseReport: "Report resolved" @@ -2827,12 +2593,6 @@ _moderationLogTypes: deleteAvatarDecoration: "Avatar decoration deleted" unsetUserAvatar: "Unset this user's avatar" unsetUserBanner: "Unset this user's banner" - createSystemWebhook: "Create SystemWebhook" - updateSystemWebhook: "Update SystemWebHook" - deleteSystemWebhook: "Delete SystemWebhook" - createAbuseReportNotificationRecipient: "Create a recipient for abuse reports" - updateAbuseReportNotificationRecipient: "Update recipients for abuse reports" - deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports" _fileViewer: title: "File details" type: "File type" @@ -2891,84 +2651,10 @@ _dataSaver: description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." _urlPreview: title: "URL preview thumbnails" - description: "URL preview thumbnail images will no longer be loaded." + description: "URL preview thumbnail images will no longer load." _code: title: "Code highlighting" description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data." -_hemisphere: - N: "Northern Hemisphere" - S: "Southern Hemisphere" - caption: "Used in some client settings to determine season." -_reversi: - reversi: "Reversi" - gameSettings: "Game settings" - chooseBoard: "Choose a board" - blackOrWhite: "Black/White" - blackIs: "{name} is playing Black" - rules: "Rules" - thisGameIsStartedSoon: "The game will begin shortly" - waitingForOther: "Waiting for opponent's turn" - waitingForMe: "Waiting for your turn" - waitingBoth: "Get ready" - ready: "Ready" - cancelReady: "Not ready" - opponentTurn: "Opponent's turn" - myTurn: "Your turn" - turnOf: "It's {name}'s turn" - pastTurnOf: "{name}'s turn" - surrender: "Surrender" - surrendered: "Surrendered" - timeout: "Out of time" - drawn: "Draw" - won: "{name} wins" - black: "Black" - white: "White" - total: "Total" - turnCount: "Turn {count}" - myGames: "My rounds" - allGames: "All rounds" - ended: "Ended" - playing: "Currently playing" - isLlotheo: "The one with fewer stones wins (Llotheo)" - loopedMap: "Looping map" - canPutEverywhere: "Tiles are placeable everywhere" - timeLimitForEachTurn: "Time limit for turn" - freeMatch: "Free Match" - lookingForPlayer: "Finding opponent..." - gameCanceled: "The game has been cancelled." - shareToTlTheGameWhenStart: "Share Game to timeline when started" - iStartedAGame: "The game has begun! #MisskeyReversi" - opponentHasSettingsChanged: "The opponent has changed their settings." - allowIrregularRules: "Irregular rules (completely free)" - disallowIrregularRules: "No irregular rules" - showBoardLabels: "Display row and column numbering on the board" - useAvatarAsStone: "Turn stones into user avatars" -_offlineScreen: - title: "Offline - cannot connect to the server" - header: "Unable to connect to the server" -_urlPreviewSetting: - title: "URL preview settings" - enable: "Enable URL preview" - timeout: "Time out when getting preview (ms)" - timeoutDescription: "If it takes longer than this value to get the preview, the preview won’t be generated." - maximumContentLength: "Maximum Content-Length (bytes)" - maximumContentLengthDescription: "If Content-Length is higher than this value, the preview won't be generated." - requireContentLength: "Generate the preview only if you could get Content-Length" - requireContentLengthDescription: "If other server doesn't return Content-Length, the preview won't be generated." - userAgent: "User-Agent" - userAgentDescription: "Sets the User-Agent to be used when retrieving previews. If left blank, the default User-Agent will be used." - summaryProxy: "Proxy endpoints that generate previews" - summaryProxyDescription: "Not CherryPick itself, but generate previews using Summaly Proxy." - summaryProxyDescription2: "The following parameters are linked to the proxy as a query string. If the proxy does not support them, the values are ignored." -_mediaControls: - pip: "Picture in Picture" - playbackRate: "Playback Speed" - loop: "Loop playback" -_contextMenu: - title: "Context menu" - app: "Application" - appWithShift: "Application with shift key" - native: "Native" _abuse: _resolver: 1hour: "one hour" @@ -2993,39 +2679,3 @@ _imageCompressionMode: noResizeCompress: "Compression without resize" resizeCompressLossy: "Resize and lossy compression" noResizeCompressLossy: "Lossy compression without resize" - -_advancedSearch: - _fileOption: - title: "File attachment status" - fileAttachedOnly: "with file" - noFile: "No file" - combined: "both" - _searchOption: - toggleNsfw: "Exclude CW" - toggleReply: "Exclude Reply" - toggleDate: "Specify date and time" - toggleAdvancedSearch: "Enable Advanced Search" - _specifyDate: - startDate: "From" - endDate: "Until" - _description: - other: "Other settings" - -_searchOrApShow: - question: "Want to search?" - search: "Search" - lookup: "Lookup" - -_dice: - rollDice: "Roll!" - diceCount: "Number of dice" - diceFaces: "Number of dice's faces" - -_isIndexable: - title: "Indexable" - description: "This is kmyblue compatible feature. If you want to prevent your account from being indexed by search engines, please disable this option." - -_altWarning: - noAltWarning: "ファイルに代替テキストが設定されていません。" - showNoAltWarning: "画像に代替テキストが設定されていない場合に警告を表示する" - postAnyWay: "投稿フォームへ" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index e2dd7c7608..6af5b58122 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -11,7 +11,7 @@ password: "Contraseña" forgotPassword: "Olvidé mi contraseña" fetchingAsApObject: "Buscando en el fediverso" ok: "OK" -gotIt: "¡Lo tengo!" +gotIt: "Entendido" cancel: "Cancelar" noThankYou: "No gracias" enterUsername: "Introduce el nombre de usuario" @@ -130,7 +130,6 @@ overwriteFromPinnedEmojis: "Sobreescribir los emojis fijados" reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir." rememberNoteVisibility: "Recordar visibilidad" attachCancel: "Quitar adjunto" -deleteFile: "Archivo eliminado" markAsSensitive: "Marcar como sensible" unmarkAsSensitive: "Desmarcar como sensible" enterFileName: "Ingrese el nombre del archivo" @@ -235,7 +234,7 @@ done: "Terminado" processing: "Procesando" preview: "Vista previa" default: "Predeterminado" -defaultValueIs: "Por defecto: {value}" +defaultValueIs: "Predeterminado" noCustomEmojis: "No hay emojis personalizados" noJobs: "No hay trabajos" federating: "Federando" @@ -302,7 +301,7 @@ location: "Lugar" theme: "Tema" themeForLightMode: "Tema para usar en Modo Linterna" themeForDarkMode: "Tema para usar en Modo Oscuro" -light: "Claro" +light: "Linterna" dark: "Oscuro" lightThemes: "Tema claro" darkThemes: "Tema oscuro" @@ -380,11 +379,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Habilitar hCaptcha" hcaptchaSiteKey: "Clave del sitio" hcaptchaSecretKey: "Clave secreta" -mcaptcha: "mCaptcha" -enableMcaptcha: "Activar mCaptcha" -mcaptchaSiteKey: "Clave del sitio" -mcaptchaSecretKey: "Clave secreta" -mcaptchaInstanceUrl: "URL del servidor mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "activar reCAPTCHA" recaptchaSiteKey: "Clave del sitio" @@ -400,7 +394,6 @@ name: "Nombre" antennaSource: "Origen de la antena" antennaKeywords: "Palabras clave para recibir" antennaExcludeKeywords: "Palabras clave para excluir" -antennaExcludeBots: "Excluir bots" antennaKeywordsDescription: "Separar con espacios es una declaración AND, separar con una linea nueva es una declaración OR" notifyAntenna: "Notificar nueva nota" withFileAntenna: "Sólo notas con archivos adjuntados" @@ -507,7 +500,6 @@ disableDrawer: "No mostrar los menús en cajones" youHaveNoGroups: "Sin grupos" joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo." 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" signinHistory: "Historial de ingresos" enableAdvancedMfm: "Habilitar MFM avanzado" @@ -646,7 +638,6 @@ medium: "Mediano" small: "Pequeño" generateAccessToken: "Generar token de acceso" permission: "Permisos" -adminPermission: "Permiso de administrador" enableAll: "Activar todo" disableAll: "Desactivar todo" tokenRequested: "Permiso de acceso a la cuenta" @@ -690,7 +681,6 @@ useGlobalSettingDesc: "Al activarse, se usará la configuración de notificacion other: "Otro" regenerateLoginToken: "Regenerar token de login" regenerateLoginTokenDescription: "Regenerar el token usado internamente durante el login. No siempre es necesario hacerlo. Al hacerlo de nuevo, se deslogueará en todos los dispositivos." -theKeywordWhenSearchingForCustomEmoji: "Palabra clave para buscar el emoji personalizado." setMultipleBySeparatingWithSpace: "Puedes añadir mas de uno, separado por espacios." fileIdOrUrl: "Id del archivo o URL" behavior: "Comportamiento" @@ -1007,7 +997,6 @@ neverShow: "No mostrar de nuevo" remindMeLater: "Recordar después" didYouLikeMisskey: "¿Te gusta CherryPick?" pleaseDonate: "{host} usa el software gratuito CherryPick. Por favor ¡Considera donar al proyecto principal para que podamos continuar!" -correspondingSourceIsAvailable: "El código fuente correspondiente se encuentra disponible en {anchor}" roles: "Roles" role: "Rol" noRole: "Rol no encontrado" @@ -1058,9 +1047,6 @@ resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" sensitiveWords: "Palabras sensibles" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea" sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." -prohibitedWords: "Palabras explícitas" -prohibitedWordsDescription: "Activa un error cuando se intenta publicar una nota que contiene una o varias palabras prohibidas. Se pueden establecer varias palabras, una por línea." -prohibitedWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." hiddenTags: "Hashtags ocultos" hiddenTagsDescription: "Selecciona las etiquetas que no se mostrarán en tendencias. Una etiqueta por línea." notesSearchNotAvailable: "No se puede buscar una nota" @@ -1079,8 +1065,6 @@ limitWidthOfReaction: "Limitar ancho de las reacciones" noteIdOrUrl: "ID o URL de la nota" video: "Video" videos: "Video" -audio: "Sonido" -audioFiles: "Sonido" dataSaver: "Ahorro de datos" accountMigration: "Migración de cuenta" accountMoved: "Este usuario se movió a una nueva cuenta:" @@ -1176,7 +1160,6 @@ showRenotes: "Mostrar renotas" edited: "Editado" notificationRecieveConfig: "Ajustes de Notificaciones" mutualFollow: "Os seguís mutuamente" -followingOrFollower: "Siguiendo o seguidor" fileAttachedOnly: "Solo notas con archivos" showRepliesToOthersInTimeline: "Mostrar respuestas a otros en la línea de tiempo" hideRepliesToOthersInTimeline: "Ocultar respuestas a otros en la línea de tiempo" @@ -1185,13 +1168,6 @@ hideRepliesToOthersInTimelineAll: "Ocultar tus respuestas a otros usuarios que s confirmShowRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres mostrar tus respuestas a otros usuarios que sigues en tu línea de tiempo?" confirmHideRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres ocultar tus respuestas a otros usuarios que sigues en tu línea de tiempo?" externalServices: "Servicios Externos" -sourceCode: "Código fuente" -sourceCodeIsNotYetProvided: "El código fuente aún no está disponible. Contacta con el administrador para solucionarlo." -repositoryUrl: "URL del repositorio" -repositoryUrlDescription: "Si estás usando CherryPick tal cual (sin cambios en el código fuente), entra en https://github.com/kokonect-link/cherrypick" -repositoryUrlOrTarballRequired: "Si no has publicado un repositorio aún, deberás publicar un tarball en su lugar. Mira el archivo .config/example.yml para más información." -feedback: "Comentarios" -feedbackUrl: "URL de comentarios" impressum: "Impressum" impressumUrl: "Impressum URL" impressumDescription: "En algunos países, como Alemania, la inclusión del operador de datos (el Impressum) es requerido legalmente para sitios web comerciales." @@ -1219,53 +1195,6 @@ remainingN: "Faltan: {n}" overwriteContentConfirm: "¿Quieres sustituir todo el contenido actual?" seasonalScreenEffect: "Efectos de pantalla asociados a estaciones" decorate: "Decorar" -addMfmFunction: "Añadir función MFM" -enableQuickAddMfmFunction: "Activar acceso rápido para añadir funciones MFM" -bubbleGame: "Bubble Game" -sfx: "Efectos de sonido" -soundWillBePlayed: "Se reproducirán efectos sonoros" -showReplay: "Ver reproducción" -replay: "Reproducir" -replaying: "Reproduciendo" -endReplay: "Terminar reproducción" -copyReplayData: "Copiar datos de reproducción" -ranking: "Clasificación" -lastNDays: "Últimos {n} días" -backToTitle: "Regresar al inicio" -hemisphere: "Región" -withSensitive: "Mostrar notas que contengan material sensible" -userSaysSomethingSensitive: "La publicación de {name} contiene material sensible" -enableHorizontalSwipe: "Deslice para cambiar de pestaña" -loading: "Cargando" -surrender: "detener" -gameRetry: "Reintentar" -notUsePleaseLeaveBlank: "Dejar en blanco si no se usa" -useTotp: "Introduce la contraseña de un solo uso" -useBackupCode: "Usar códigos de respaldo" -launchApp: "Ejecutar la app" -useNativeUIForVideoAudioPlayer: "Usar la interfaz del navegador cuando se reproduce audio y vídeo" -keepOriginalFilename: "Mantener el nombre original del archivo" -noDescription: "No hay descripción" -alwaysConfirmFollow: "Confirmar siempre cuando se sigue a alguien" -_delivery: - stop: "Suspendido" - _type: - none: "Publicando" -_bubbleGame: - howToPlay: "Cómo jugar" - hold: "Mantener" - _score: - score: "Puntos" - scoreYen: "Cantidad de dinero ganada" - highScore: "Puntuación más alta" - maxChain: "Número máximo de cadenas" - yen: "{yen} Yenes" - estimatedQty: "{qty} Piezas" - scoreSweets: "{onigiriQtyWithUnit} Onigiris" - _howToPlay: - section1: "Ajuste la posición y deje caer el objeto en la caja" - section2: "Cuando dos objetos del mismo tipo se tocan, cambian a otro tipo y consigues puntos" - section3: "El juego termina cuando la caja se desborda de objetos. ¡Intenta conseguir una puntuación alta al juntar objetos mientras evitas desbordar la caja!" _announcement: forExistingUsers: "Solo para usuarios registrados" forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán." @@ -1379,7 +1308,7 @@ _serverSettings: _accountMigration: moveFrom: "Trasladar de otra cuenta a ésta" moveFromSub: "Crear un alias para otra cuenta." - moveFromLabel: "Cuenta desde la que se realiza el traslado #{n}" + moveFromLabel: "Cuenta desde la que se realiza el traslado:" moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com" moveTo: "Mover esta cuenta a una nueva" moveToLabel: "Cuenta destino:" @@ -1447,7 +1376,7 @@ _achievements: _login3: title: "Principiante I" description: "Días desde el inicio de sesión: 3" - flavor: "Desde hoy, soy Cherrypikero" + flavor: "Desde hoy, soy Misskero" _login7: title: "Principiante II" description: "Días desde el inicio de sesión: 7" @@ -1639,13 +1568,6 @@ _achievements: _tutorialCompleted: title: "Diploma del Curso Básico de CherryPick" description: "Tutorial completado" - _bubbleGameExplodingHead: - title: "🤯" - description: "El objeto más grande en el juego de burbujas" - _bubbleGameDoubleExplodingHead: - title: "Doble 🤯" - description: "Dos de los objetos más grandes en el juego de burbujas al mismo tiempo" - flavor: "Puedes llenar el bento un poco de esta forma 🤯 🤯." _role: new: "Crear rol" edit: "Editar rol" @@ -1686,7 +1608,6 @@ _role: gtlAvailable: "Explorar la línea de tiempo global" ltlAvailable: "Explorar la línea de tiempo local" canPublicNote: "Permitir la publicación" - mentionMax: "Número máximo de menciones en una nota" canInvite: "Puede crear códigos de invitación" inviteLimit: "Límite de invitaciones" inviteLimitCycle: "Enfriamiento del límite de invitaciones" @@ -1710,13 +1631,8 @@ _role: canUseTranslator: "Uso de traductor" avatarDecorationLimit: "Número máximo de decoraciones de avatar" _condition: - roleAssignedTo: "Asignado a roles manuales" isLocal: "Usuario local" isRemote: "Usuario remoto" - isCat: "Usuarios Gato" - isBot: "Usuarios Bot" - isSuspended: "Usuario suspendido" - isLocked: "Cuentas privadas" createdLessThan: "Menos de X han pasado desde la creación de la cuenta" createdMoreThan: "Más de X han pasado desde la creación de la cuenta" followersLessThanOrEq: "Tiene X o menos seguidores" @@ -1786,7 +1702,6 @@ _plugin: installWarn: "Por favor no instale plugins que no son de confianza" manage: "Gestionar plugins" viewSource: "Ver la fuente" - viewLog: "Ver log" _preferencesBackups: list: "Respaldos creados" saveNew: "Guardar nuevo respaldo" @@ -1816,8 +1731,6 @@ _aboutMisskey: contributors: "Principales colaboradores" allContributors: "Todos los colaboradores" source: "Código fuente" - original: "Original" - thisIsModifiedVersion: "{name} usa una versión modificada de CherryPick." translation: "Traducir Misskey" donate: "Donar a Misskey" morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰" @@ -2005,6 +1918,8 @@ _sfx: notification: "Notificaciones" chat: "Chat" chatBg: "Chat (Fondo)" + antenna: "Antena receptora" + channel: "Notificaciones del canal" reaction: "Al seleccionar una reacción" _soundSettings: driveFile: "Usar un archivo de audio en Drive" @@ -2042,6 +1957,7 @@ _2fa: registerTOTP: "Registrar aplicación autenticadora" step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra." step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla." + step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app.\nTocar este código QR te permitirá registrar la autenticación 2FA a tu llave de seguridad o aplicación autenticadora." step2Uri: "Si usas una aplicación de escritorio, introduce en ella la siguiente URL." step3Title: "Ingresa un código de autenticación" step3: "Para terminar, ingrese el token mostrado en la aplicación." @@ -2065,7 +1981,6 @@ _2fa: backupCodesDescription: "En caso de que no puedas usar tu aplicación de autenticación, podrás usar los códigos de respaldo que figuran abajo para acceder a tu cuenta. Asegúrate de guardar en lugar seguro los códigos de respaldo. Cada uno de los códigos de respaldo es de un solo uso." backupCodeUsedWarning: "Has usado todos los códigos de respaldo. Si dejas de tener acceso a tu aplicación de autenticación, no podrás volver a iniciar sesión en tu cuenta. Por favor, reconfigura tu aplicación de autenticación lo antes posible." backupCodesExhaustedWarning: "Has usado todos los códigos de respaldo. Si dejas de tener acceso a tu aplicación de autenticación, no podrás volver a iniciar sesión en la cuenta que figura arriba. Por favor, reconfigura tu aplicación de autenticación lo antes posible." - moreDetailedGuideHere: "Guía detallada" _permissions: "read:account": "Ver información de la cuenta" "write:account": "Editar información de la cuenta" @@ -2103,54 +2018,6 @@ _permissions: "write:flash": "Editar Plays" "read:flash-likes": "Ver los Play que me gustan" "write:flash-likes": "Editar lista de Play que me gustan" - "read:admin:abuse-user-reports": "Ver reportes de usuarios" - "write:admin:delete-account": "Eliminar cuentas de usuario" - "write:admin:delete-all-files-of-a-user": "Eliminar todos los archivos de un usuario" - "read:admin:index-stats": "Ver datos indexados" - "read:admin:table-stats": "Ver estadísticas de las tablas de la base de datos" - "read:admin:user-ips": "Ver dirección IP de usuario" - "read:admin:meta": "Ver metadatos de la instancia" - "write:admin:reset-password": "Restablecer contraseñas de usuario" - "write:admin:resolve-abuse-user-report": "Resolución de reportes de usuario" - "write:admin:send-email": "Enviar email" - "read:admin:server-info": "Ver información del servidor" - "read:admin:show-moderation-log": "Ver log de moderación" - "read:admin:show-user": "Ver información privada de usuario" - "write:admin:suspend-user": "Suspender cuentas de usuario" - "write:admin:unset-user-avatar": "Quitar avatares de usuario" - "write:admin:unset-user-banner": "Quitar banner de usuarios" - "write:admin:unsuspend-user": "Quitar suspensión de cuentas de usuario" - "write:admin:meta": "Edición de metadatos de la instancia" - "write:admin:user-note": "Moderación de notas" - "write:admin:roles": "Edición de roles de usuario" - "read:admin:roles": "Ver roles de usuario" - "write:admin:relays": "Edición de relays" - "read:admin:relays": "Ver relays" - "write:admin:invite-codes": "Edición de códigos de invitación" - "read:admin:invite-codes": "Ver códigos de invitación" - "write:admin:announcements": "Edición de anuncios" - "read:admin:announcements": "Ver anuncios" - "write:admin:avatar-decorations": "Edición de decoración de avatares" - "read:admin:avatar-decorations": "Ver decoraciones de avatar" - "write:admin:federation": "Edición de federación de instancias" - "write:admin:account": "Edición de cuentas de usuario" - "read:admin:account": "Ver cuentas de usuario" - "write:admin:emoji": "Edición de emojis" - "read:admin:emoji": "Ver emojis" - "write:admin:queue": "Edición de cola de tareas" - "read:admin:queue": "Ver cola de tareas" - "write:admin:promo": "Edición de promociones" - "write:admin:drive": "Edición de Drive de usuarios" - "read:admin:drive": "Ver Drive de usuarios" - "read:admin:stream": "Usar la API de Websocket para administradores" - "write:admin:ad": "Edición de anuncios" - "read:admin:ad": "Ver anuncios" - "write:invite-codes": "Crear códigos de invitación" - "read:invite-codes": "Ver códigos de invitación" - "write:clip-favorite": "Marcar me gusta en clips" - "read:clip-favorite": "Ver los clips que me gustan" - "read:federation": "Ver instancias federadas" - "write:report-abuse": "Crear reportes de usuario" _auth: shareAccessTitle: "Permisos de la aplicación" shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?" @@ -2273,7 +2140,6 @@ _profile: _exportOrImport: allNotes: "Todas las notas" favoritedNotes: "Notas favoritas" - clips: "Clip" followingList: "Siguiendo" muteList: "Silenciados" blockingList: "Bloqueados" @@ -2328,7 +2194,6 @@ _play: title: "Título" script: "Script" summary: "Descripción" - visibilityDescription: "Poniéndola como privada significa que no será visible en tu perfil, pero cualquiera que tenga la URL aún podrá acceder a ella." _pages: newPage: "Crear página" editPage: "Editar página" @@ -2373,8 +2238,6 @@ _pages: section: "Sección" image: "Imagen" button: "Botón" - dynamic: "Bloques Dinámicos" - dynamicDescription: "Los bloques dinámicos están obsoletos. A partir de ahora, utiliza {play} por favor." note: "Nota embebida" _note: id: "Id de la nota" @@ -2471,6 +2334,7 @@ _webhookSettings: createWebhook: "Crear Webhook" name: "Nombre" secret: "Secreto" + events: "Eventos de webhook" active: "Activado" _events: follow: "Cuando se sigue a alguien" @@ -2480,10 +2344,6 @@ _webhookSettings: renote: "Cuando reciba un \"re-note\"" reaction: "Cuando se recibe una reacción" mention: "Cuando hay una mención" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Correo" _moderationLogTypes: createRole: "Rol creado" deleteRole: "Rol eliminado" @@ -2582,18 +2442,3 @@ _dataSaver: _code: title: "Resaltar código" description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos." -_hemisphere: - N: "Hemisferio norte" - S: "Hemisferio sur" -_reversi: - reversi: "Reversi" - won: "{name} ha ganado" - total: "Total" -_urlPreviewSetting: - timeout: "Timeout de la carga de vista previa de las URLs (ms)" - maximumContentLength: "Content-Length Máximo (bytes)" - userAgent: "User-Agent" -_mediaControls: - pip: "Picture in Picture" - playbackRate: "Velocidad de reproducción" - loop: "Reproducción en bucle" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index cce93847b4..e6d6ec506e 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1,8 +1,8 @@ --- _lang_: "Français" headlineMisskey: "Réseau relié par des notes" -introMisskey: "Bienvenue ! CherryPick est un service de microblogage décentralisé, libre et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à l’instant présent, autour de vous avec les autres 📡\nLa fonction « réactions », vous permet également d’ajouter une réaction rapide aux notes des autres utilisateur·rice·s 👍\nExplorons un nouveau monde 🚀" -poweredByMisskeyDescription: "{name} est l'un des services propulsés par la plateforme ouverte CherryPick (appelée \"instance CherryPick\")." +introMisskey: "Bienvenue ! CherryPick est un service de microblogage décentralisé, libre et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à l’instant présent, autour de vous avec les autres 📡\nLa fonction « réactions », vous permet également d’ajouter une réaction rapide aux notes des autres utilisateur·rice·s 👍\nExplorons un nouveau monde 🚀" +poweredByMisskeyDescription: "{nom} est l'un des services propulsés par la plateforme ouverte CherryPick (appelée \"instance CherryPick\")." monthAndDay: "{day}/{month}" search: "Rechercher" notifications: "Notifications" @@ -129,8 +129,7 @@ overwriteFromPinnedEmojisForReaction: "Remplacer par les émojis épinglés pour overwriteFromPinnedEmojis: "Remplacer par les émojis épinglés globalement" reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter." rememberNoteVisibility: "Se souvenir de la visibilité des notes" -attachCancel: "Supprimer le fichier joint" -deleteFile: "Fichier supprimé" +attachCancel: "Supprimer le fichier attaché" markAsSensitive: "Marquer comme sensible" unmarkAsSensitive: "Supprimer le marquage comme sensible" enterFileName: "Entrer le nom du fichier" @@ -169,7 +168,7 @@ cacheRemoteSensitiveFilesDescription: "Si vous désactivez ce paramètre, les fi flagAsBot: "Ce compte est un robot" flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de CherryPick pour traiter ce compte comme un robot." flagAsCat: "Ce compte est un chat" -flagAsCatDescription: "Miaou miaou miaou ?" +flagAsCatDescription: "Activer l'option \" Je suis un chat \" pour ce compte." flagShowTimelineReplies: "Afficher les réponses dans le fil" flagShowTimelineRepliesDescription: "Affiche les réponses des utilisateurs aux notes des autres utilisateurs dans la timeline si cette option est activée." autoAcceptFollowed: "Accepter automatiquement les demandes d’abonnement venant d’utilisateur·rice·s que vous suivez" @@ -380,11 +379,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Activer hCaptcha" hcaptchaSiteKey: "Clé du site" hcaptchaSecretKey: "Clé secrète" -mcaptcha: "mCaptcha" -enableMcaptcha: "Activer mCaptcha" -mcaptchaSiteKey: "Clé du site" -mcaptchaSecretKey: "Clé secrète" -mcaptchaInstanceUrl: "URL de l'instance de mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Activer reCAPTCHA" recaptchaSiteKey: "Clé du site" @@ -400,10 +394,9 @@ name: "Nom" antennaSource: "Source de l’antenne" antennaKeywords: "Mots clés à recevoir" antennaExcludeKeywords: "Mots clés à exclure" -antennaExcludeBots: "Exclure les comptes robot" antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." notifyAntenna: "Me notifier pour les nouvelles notes" -withFileAntenna: "Notes ayant des fichiers joints uniquement" +withFileAntenna: "Notes ayant des attachements uniquement" enableServiceworker: "Activer ServiceWorker" antennaUsersDescription: "Saisissez un seul nom d’utilisateur·rice par ligne" caseSensitive: "Sensible à la casse" @@ -507,7 +500,6 @@ disableDrawer: "Les menus ne s'affichent pas dans le tiroir" youHaveNoGroups: "Vous n’avez aucun groupe" joinOrCreateGroup: "Vous pouvez être invité·e à rejoindre des groupes existants ou créer votre propre nouveau groupe." showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol" -showReactionsCount: "Afficher le nombre de réactions des notes" noHistory: "Pas d'historique" signinHistory: "Historique de connexion" enableAdvancedMfm: "Activer la MFM avancée" @@ -540,7 +532,7 @@ hideThisNote: "Masquer cette note" showFeaturedNotesInTimeline: "Afficher les notes des Tendances dans le fil d'actualité" objectStorage: "Stockage d'objets" useObjectStorage: "Utiliser le stockage d'objets" -objectStorageBaseUrl: "URL de base" +objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "Préfixe d’URL utilisé pour construire l’URL vers le référencement d’objet (média). Spécifiez son URL si vous utilisez un CDN ou un proxy, sinon spécifiez l’adresse accessible au public selon le guide de service que vous allez utiliser. P.ex. 'https://.s3.amazonaws.com' pour AWS S3 et 'https://storage.googleapis.com/' pour GCS." objectStorageBucket: "Bucket" objectStorageBucketDesc: "Veuillez spécifier le nom du compartiment utilisé sur le service configuré." @@ -555,7 +547,6 @@ objectStorageUseSSLDesc: "Désactivez cette option si vous n'utilisez pas HTTPS objectStorageUseProxy: "Se connecter via proxy" objectStorageUseProxyDesc: "Désactivez cette option si vous n'utilisez pas de proxy pour la connexion API" objectStorageSetPublicRead: "Régler sur « public » lors de l'envoi" -s3ForcePathStyleDesc: "Si s3ForcePathStyle est activé, le nom du compartiment doit être spécifié comme une partie du chemin de l'URL plutôt que le nom d'hôte. Il faudra peut-être l'activer lors de l'utilisation d'une instance de Minio autohébergée, etc." serverLogs: "Journal du serveur" deleteAll: "Supprimer tout" showFixedPostForm: "Afficher le formulaire de publication en haut du fil d'actualité" @@ -645,8 +636,7 @@ large: "Grand" medium: "Moyen" small: "Petit" generateAccessToken: "Générer un jeton d'accès" -permission: "Autorisations " -adminPermission: "Droits de l'administrateur" +permission: "Autorisations " enableAll: "Tout activer" disableAll: "Tout désactiver" tokenRequested: "Autoriser l'accès au compte" @@ -670,7 +660,7 @@ testEmail: "Tester la distribution de courriel" wordMute: "Filtre de mots" hardWordMute: "Filtre de mots dur" regexpError: "Erreur d’expression régulière" -regexpErrorDescription: "Une erreur s'est produite dans l'expression régulière sur la ligne {line} de votre mot muet {tab} :" +regexpErrorDescription: "Une erreur s'est produite dans l'expression régulière sur la ligne {ligne} de votre mot muet {tab} :" instanceMute: "Instance en sourdine" userSaysSomething: "{name} a dit quelque chose" makeActive: "Activer" @@ -690,7 +680,6 @@ useGlobalSettingDesc: "S'il est activé, les paramètres de notification de votr other: "Autre" regenerateLoginToken: "Régénérer le jeton de connexion" regenerateLoginTokenDescription: "Générer un nouveau jeton d'authentification. Cette opération ne devrait pas être nécessaire ; lors de la génération d'un nouveau jeton, tous les appareils seront déconnectés. " -theKeywordWhenSearchingForCustomEmoji: "Ce mot-clé est utilisé lors de la recherche des émojis personnalisés." setMultipleBySeparatingWithSpace: "Vous pouvez en définir plusieurs, en les séparant par des espaces." fileIdOrUrl: "ID du fichier ou URL" behavior: "Comportement" @@ -719,7 +708,7 @@ system: "Système" switchUi: "Modifier l'interface utilisateur" desktop: "Bureau" clip: "Clip" -createNew: "Créer" +createNew: "Créer nouveau" optional: "Facultatif" createNewClip: "Créer un nouveau clip" unclip: "Supprimer le clip" @@ -1007,7 +996,6 @@ neverShow: "Ne plus afficher" remindMeLater: "Peut-être plus tard" didYouLikeMisskey: "Avez-vous aimé CherryPick ?" pleaseDonate: "CherryPick est le logiciel libre utilisé par {host}. Merci de faire un don pour que nous puissions continuer à le développer !" -correspondingSourceIsAvailable: "Le code source correspondant est disponible à {anchor}" roles: "Rôles" role: "Rôles" noRole: "Aucun rôle" @@ -1022,7 +1010,6 @@ youCannotCreateAnymore: "Vous avez atteint la limite de création." cannotPerformTemporary: "Temporairement indisponible" cannotPerformTemporaryDescription: "Temporairement indisponible puisque le nombre d'opérations dépasse la limite. Veuillez patienter un peu, puis réessayer." invalidParamError: "Paramètres invalides" -invalidParamErrorDescription: "Les paramètres de la requête sont invalides. Il s'agit généralement d'un bogue, mais cela peut aussi être causé par un excès de caractères ou quelque chose de similaire." permissionDeniedError: "Opération refusée" permissionDeniedErrorDescription: "Ce compte n'a pas la permission d'effectuer cette opération." preset: "Préréglage" @@ -1036,7 +1023,6 @@ thisPostMayBeAnnoyingCancel: "Annuler" thisPostMayBeAnnoyingIgnore: "Publier quand-même" collapseRenotes: "Réduire les renotes déjà vues" internalServerError: "Erreur interne du serveur" -internalServerErrorDescription: "Une erreur inattendue s'est produite sur le serveur." copyErrorInfo: "Copier les détails de l’erreur" joinThisServer: "S'inscrire à cette instance" exploreOtherServers: "Trouver une autre instance" @@ -1056,20 +1042,12 @@ nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non sensibles seulement (mentions j' rolesAssignedToMe: "Rôles attribués à moi" resetPasswordConfirm: "Souhaitez-vous réinitialiser votre mot de passe ?" sensitiveWords: "Mots sensibles" -sensitiveWordsDescription: "Définir la visibilité des notes contenant un mot défini ici au fil principal automatiquement. Vous pouvez définir plusieurs valeurs en les séparant par des sauts de ligne." -sensitiveWordsDescription2: "Séparer par une espace pour créer une expression AND ; entourer de barres obliques pour créer une expression régulière." -prohibitedWords: "Mots interdits" -prohibitedWordsDescription: "Publier une note contenant un mot défini ici produira une erreur. Vous pouvez définir plusieurs valeurs en les séparant par des sauts de ligne." -prohibitedWordsDescription2: "Séparer par une espace pour créer une expression AND ; entourer de barres obliques pour créer une expression régulière." hiddenTags: "Hashtags cachés" hiddenTagsDescription: "Les hashtags définis ne s'afficheront pas dans les tendances. Vous pouvez définir plusieurs hashtags en faisant un saut de ligne." notesSearchNotAvailable: "La recherche de notes n'est pas disponible." license: "Licence" -unfavoriteConfirm: "Vraiment supprimer des favoris ?" myClips: "Mes clips" drivecleaner: "Nettoyeur du Disque" -retryAllQueuesNow: "Réessayer tous les fils d'attente immédiatement" -retryAllQueuesConfirmTitle: "Vraiment réessayer ?" retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur." enableChartsForRemoteUser: "Générer les graphiques pour les utilisateurs distants" enableChartsForFederatedInstances: "Générer les graphiques pour les instances distantes" @@ -1079,8 +1057,6 @@ limitWidthOfReaction: "Limiter la largeur maximale des réactions et les affiche noteIdOrUrl: "Identifiant de la note ou URL" video: "Vidéo" videos: "Vidéos" -audio: "Audio" -audioFiles: "Fichiers audio" dataSaver: "Économiseur de données" accountMigration: "Migration de compte" accountMoved: "Cet·te utilisateur·rice a migré son compte vers :" @@ -1105,11 +1081,9 @@ pleaseConfirmBelowBeforeSignup: "Pour vous inscrire sur cette instance, vous dev pleaseAgreeAllToContinue: "Pour continuer, veuillez accepter tous les champs ci-dessus." continue: "Continuer" preservedUsernames: "Noms d'utilisateur·rice réservés" -preservedUsernamesDescription: "Énumérez les noms d'utilisateur à réserver, séparés par des nouvelles lignes. Les noms d'utilisateur spécifiés ici ne seront plus utilisables lors de la création d'un compte, sauf la création manuelle par un administrateur. De plus, les comptes existants ne seront pas affectés." createNoteFromTheFile: "Rédiger une note de ce fichier" archive: "Archive" channelArchiveConfirmTitle: "Voulez-vous vraiment archiver {name} ?" -channelArchiveConfirmDescription: "Une fois archivé, le canal n'apparaîtra plus dans la liste des canaux ni dans les résultats de recherche, et la publication des nouvelles notes sera impossible." thisChannelArchived: "Ce canal a été archivé." displayOfNote: "Affichage de la note" initialAccountSetting: "Configuration initiale du profil" @@ -1121,29 +1095,12 @@ specifyUser: "Spécifier l'utilisateur·rice" failedToPreviewUrl: "Aperçu d'URL échoué" update: "Mettre à jour" rolesThatCanBeUsedThisEmojiAsReaction: "Rôles qui peuvent utiliser cet émoji comme réaction" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si aucun rôle n'est spécifié, tout le monde peut utiliser cet émoji comme réaction." -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Il faut un rôle public." -cancelReactionConfirm: "Supprimez la réaction ?" -changeReactionConfirm: "Changer la réaction ?" later: "Plus tard" goToMisskey: "Retour vers CherryPick" additionalEmojiDictionary: "Dictionnaires d'émojis additionnels" installed: "Installé" branding: "Image de marque" -enableServerMachineStats: "Publier les statistiques du matériel du serveur" -enableIdenticonGeneration: "Générer les identicons des utilisateurs" -turnOffToImprovePerformance: "Désactiver peut améliorer la performance." -createInviteCode: "Créer un code d'invitation" -createWithOptions: "Options" -createCount: "Quantité à créer" -inviteCodeCreated: "Code d'invitation créé" -inviteLimitExceeded: "Vous avez atteint la limite de codes d'invitation que vous pouvez générer." -createLimitRemaining: "Codes d'invitation pouvant être créés : {limit} restants" -inviteLimitResetCycle: "Vous pouvez créer jusqu'à {limit} codes d'invitation en {time}." expirationDate: "Date d’expiration" -noExpirationDate: "Ne pas expirer" -inviteCodeUsedAt: "Code d'invitation utilisé à" -registeredUserUsingInviteCode: "Code d'invitation utilisé par" waitingForMailAuth: "En attente de la vérification de l'adresse courriel" inviteCodeCreator: "Créateur·rice de ce code d'invitation" usedAt: "Utilisé le" @@ -1152,23 +1109,17 @@ used: "Utilisé" expired: "Expiré" doYouAgree: "Êtes-vous d’accord ?" beSureToReadThisAsItIsImportant: "Assurez-vous de le lire ; c'est important." -iHaveReadXCarefullyAndAgree: "J'ai lu le contenu de « {x} » et donne mon accord." dialog: "Dialogue" icon: "Avatar" forYou: "Pour vous" currentAnnouncements: "Annonces actuelles" pastAnnouncements: "Annonces passées" -youHaveUnreadAnnouncements: "Il y a des annonces non lues." -useSecurityKey: "Suivez les instructions de votre navigateur ou de votre appareil pour utiliser une clé de sécurité ou une clé d'accès." -replies: "Réponses" -renotes: "Renotes" +replies: "Répondre" +renotes: "Renoter" loadReplies: "Inclure les réponses" loadConversation: "Afficher la conversation" pinnedList: "Liste épinglée" -keepScreenOn: "Garder l'écran toujours allumé" -verifiedLink: "Votre propriété de ce lien a été vérifiée" notifyNotes: "Notifier à propos des nouvelles notes" -unnotifyNotes: "Ne pas notifier pour la publication des notes" authentication: "Authentification" authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer" dateAndTime: "Date et heure" @@ -1176,8 +1127,6 @@ showRenotes: "Afficher les renotes" edited: "Modifié" notificationRecieveConfig: "Paramètres des notifications" mutualFollow: "Abonnement mutuel" -followingOrFollower: "Abonnement ou abonné" -fileAttachedOnly: "Avec fichiers joints seulement" showRepliesToOthersInTimeline: "Afficher les réponses aux autres dans le fil" hideRepliesToOthersInTimeline: "Masquer les réponses aux autres dans le fil" showRepliesToOthersInTimelineAll: "Afficher les réponses de toutes les personnes que vous suivez dans le fil" @@ -1185,12 +1134,6 @@ hideRepliesToOthersInTimelineAll: "Masquer les réponses de toutes les personnes confirmShowRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment afficher les réponses de toutes les personnes que vous suivez dans le fil ?" confirmHideRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment masquer les réponses de toutes les personnes que vous suivez dans le fil ?" externalServices: "Services externes" -sourceCode: "Code source" -sourceCodeIsNotYetProvided: "Le code source n'est pas encore disponible. Veuillez signaler ce problème aux administrateurs." -repositoryUrl: "URL du dépôt" -repositoryUrlDescription: "Entrez l'URL du dépôt où se trouve le code source ici. Si vous utilisez CherryPick tel quel (sans changer le code source), entrez https://github.com/kokonect-link/cherrypick" -feedback: "Commentaires" -feedbackUrl: "URL pour les commentaires" impressum: "Impressum" impressumUrl: "URL de l'impressum" impressumDescription: "Dans certains pays comme l'Allemagne, il est obligatoire d'afficher les informations sur l'opérateur d'un site (un impressum)." @@ -1218,60 +1161,17 @@ remainingN: "Restants : {n}" overwriteContentConfirm: "Voulez-vous remplacer le contenu actuel ?" seasonalScreenEffect: "Effet d'écran saisonnier" decorate: "Décorer" -addMfmFunction: "Insérer MFM" -enableQuickAddMfmFunction: "Afficher le sélecteur de MFM avancé" -bubbleGame: "Jeu de bulles" -sfx: "Effets sonores" -soundWillBePlayed: "Le son sera joué" -showReplay: "Voir le replay" -replay: "Rediffusion" -replaying: "En cours de rediffusion" -endReplay: "Arrêter la rediffusion" -copyReplayData: "Copier les données de la rediffusion" -ranking: "Classement" -lastNDays: "Derniers {n} jours" -backToTitle: "Retourner au titre" -hemisphere: "Votre région" -withSensitive: "Afficher les notes contenant des fichiers joints sensibles" -userSaysSomethingSensitive: "Note de {name} contenant des fichiers joints sensibles" -enableHorizontalSwipe: "Glisser pour changer d'onglet" -loading: "Chargement en cours" -surrender: "Annuler" -gameRetry: "Réessayer" -_delivery: - stop: "Suspendu·e" - _type: - none: "Publié" -_bubbleGame: - howToPlay: "Comment jouer" - hold: "Réserver" - _score: - score: "Score" - scoreYen: "Montant gagné" - highScore: "Meilleur score" - maxChain: "Nombre maximum de chaînes" - yen: "{yen} yens" - estimatedQty: "{qty} pièces" _announcement: - forExistingUsers: "Pour les utilisateurs existants seulement" - needConfirmationToRead: "Exiger la confirmation de la lecture" - needConfirmationToReadDescription: "Si activé, afficher un dialogue de confirmation quand l'annonce est marquée comme lue. Aussi, elle sera exclue de « marquer tout comme lu » ." - end: "Archiver l'annonce" - tooManyActiveAnnouncementDescription: "Un grand nombre d'annonces actives peut baisser l'expérience utilisateur. Considérez d'archiver les annonces obsolètes." readConfirmTitle: "Marquer comme lu ?" - readConfirmText: "Cela marquera le contenu de « {title} » comme lu." shouldNotBeUsedToPresentPermanentInfo: "Puisque cela pourrait nuire considérablement à l'expérience utilisateur pour les nouveaux utilisateurs, il est recommandé d'utiliser les annonces pour afficher des informations temporaires plutôt que des informations persistantes." dialogAnnouncementUxWarn: "Avoir deux ou plus annonces de style dialogue en même temps pourrait nuire considérablement à l'expérience utilisateur. Veuillez les utiliser avec caution." silence: "Ne pas me notifier" silenceDescription: "Si activée, vous ne recevrez pas de notifications sur les annonces et n'aurez pas besoin de les marquer comme lues." _initialAccountSetting: - accountCreated: "Votre compte a été créé avec succès !" - letsStartAccountSetup: "Procédons au réglage initial du compte." - letsFillYourProfile: "Commençons par configurer votre profil !" profileSetting: "Paramètres du profil" privacySetting: "Paramètres de confidentialité" initialAccountSettingCompleted: "Configuration du profil terminée avec succès !" - youCanContinueTutorial: "Vous pouvez procéder au tutoriel sur l'utilisation de {name}(CherryPick) ou vous arrêter ici et commencer à l'utiliser immédiatement." + youCanContinueTutorial: "Vous pouvez procéder au tutoriel sur l'utilisation de {nom}(CherryPick) ou vous arrêter ici et commencer à l'utiliser immédiatement." startTutorial: "Démarrer le tutoriel" skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?" _initialTutorial: @@ -1335,7 +1235,7 @@ _initialTutorial: doItToContinue: "Marquez le fichier joint comme sensible pour procéder." _done: title: "Le tutoriel est terminé ! 🎉" - description: "Les fonctionnalités introduites ici ne sont que quelques-unes. Pour savoir plus sur l'utilisation de CherryPick, veuillez consulter {link}." + description: "Les fonctionnalités introduites ici ne sont que quelques-unes. Pour savoir plus sur l'utilisation de CherryPick, veuillez consulter {lien}." _timelineDescription: home: "Sur le fil principal, vous pouvez voir les notes des utilisateurs auxquels vous êtes abonné·e." local: "Sur le fil local, vous pouvez voir les notes de tous les utilisateurs sur cette instance." @@ -1356,7 +1256,6 @@ _accountMigration: startMigration: "Migrer" movedTo: "Compte vers lequel vous migrez :" _achievements: - earnedAt: "Date d'obtention" _types: _notes1: title: "Je viens tout juste de configurer mon msky" @@ -1397,13 +1296,10 @@ _achievements: title: "Régulier III" description: "Se connecter pour un total de 400 jours" _login500: - title: "Expert I" description: "Se connecter pour un total de 500 jours" _login600: - title: "Expert II" description: "Se connecter pour un total de 600 jours" _login700: - title: "Expert III" description: "Se connecter pour un total de 700 jours" _login800: description: "Se connecter pour un total de 800 jours" @@ -1445,7 +1341,7 @@ _achievements: _followers1000: title: "Influenceur·euse" description: "Obtenir plus de 1000 abonné·e·s" - _iLoveCherryPick: + _iLoveMisskey: title: "J’adore CherryPick" description: "Publication « J’❤ #CherryPick »" flavor: "L'équipe de développement de CherryPick apprécie vraiment votre aide !" @@ -1498,14 +1394,11 @@ _role: edit: "Modifier le rôle" name: "Nom du rôle" description: "Description du rôle" - permission: "Autorisations du rôle" + permission: "Rôle et autorisations" assignTarget: "Attribuer" - manual: "Manuel" manualRoles: "Rôles manuels" - conditional: "Conditionnel" conditionalRoles: "Rôles conditionnels" condition: "Condition" - isConditionalRole: "Ceci est un rôle conditionnel." isPublic: "Rôle public" options: "Options" policies: "Stratégies" @@ -1796,6 +1689,8 @@ _sfx: notification: "Notifications" chat: "Discuter" chatBg: "Discussion (arrière-plan)" + antenna: "Réception de l’antenne" + channel: "Notifications de canal" reaction: "Lors de la sélection de la réaction" _soundSettings: driveFile: "Utiliser un effet sonore sur le Disque" @@ -1989,7 +1884,6 @@ _profile: avatarDecorationMax: "Vous pouvez mettre au plus {max} décorations d'avatar." _exportOrImport: allNotes: "Toutes les notes" - clips: "Clip" followingList: "Abonnements" muteList: "Comptes masqués" blockingList: "Comptes bloqués" @@ -2102,7 +1996,7 @@ _notification: unreadAntennaNote: "Antenne {name}" roleAssigned: "Rôle attribué" emptyPushNotificationMessage: "Les notifications push ont été mises à jour" - achievementEarned: "Accomplissement déverrouillé" + achievementEarned: "Accomplissement" testNotification: "Tester la notification" reactedBySomeUsers: "{n} utilisateur·rice·s ont réagi" renotedBySomeUsers: "{n} utilisateur·rice·s ont renoté" @@ -2120,7 +2014,7 @@ _notification: followRequestAccepted: "Demande d'abonnement acceptée" groupInvited: "Invitation à un groupe" roleAssigned: "Rôle reçu" - achievementEarned: "Déverrouillage d'accomplissement" + achievementEarned: "Accomplissement" app: "Notifications provenant des apps" _actions: followBack: "Suivre" @@ -2159,10 +2053,6 @@ _drivecleaner: _webhookSettings: name: "Nom" active: "Activé" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-mail " _moderationLogTypes: createRole: "Rôle créé" deleteRole: "Rôle supprimé" @@ -2261,6 +2151,3 @@ _dataSaver: _code: title: "Mise en évidence du code" description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données." -_reversi: - waitingBoth: "Préparez-vous" - total: "Total" diff --git a/locales/generateDTS.js b/locales/generateDTS.js index 49807144ec..d3afdd6e15 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -6,176 +6,54 @@ import ts from 'typescript'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const parameterRegExp = /\{(\w+)\}/g; - -function createMemberType(item) { - if (typeof item !== 'string') { - return ts.factory.createTypeLiteralNode(createMembers(item)); - } - const parameters = Array.from( - item.matchAll(parameterRegExp), - ([, parameter]) => parameter, - ); - return parameters.length - ? ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ParameterizedString'), - [ - ts.factory.createUnionTypeNode( - parameters.map((parameter) => - ts.factory.createStringLiteral(parameter), - ), - ), - ], - ) - : ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); -} function createMembers(record) { - return Object.entries(record).map(([k, v]) => { - const node = ts.factory.createPropertySignature( + return Object.entries(record) + .map(([k, v]) => ts.factory.createPropertySignature( undefined, ts.factory.createStringLiteral(k), undefined, - createMemberType(v), - ); - if (typeof v === 'string') { - ts.addSyntheticLeadingComment( - node, - ts.SyntaxKind.MultiLineCommentTrivia, - `* - * ${v.replace(/\n/g, '\n * ')} - `, - true, - ); - } - return node; - }); + typeof v === 'string' + ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) + : ts.factory.createTypeLiteralNode(createMembers(v)), + )); } export default function generateDTS() { const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); const members = createMembers(locale); const elements = [ - ts.factory.createVariableStatement( - [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier('kParameters'), - undefined, - ts.factory.createTypeOperatorNode( - ts.SyntaxKind.UniqueKeyword, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword), - ), - undefined, - ), - ], - ts.NodeFlags.Const, - ), - ), ts.factory.createInterfaceDeclaration( [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier('ParameterizedString'), - [ - ts.factory.createTypeParameterDeclaration( - undefined, - ts.factory.createIdentifier('T'), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ), - ], - undefined, - [ - ts.factory.createPropertySignature( - undefined, - ts.factory.createComputedPropertyName( - ts.factory.createIdentifier('kParameters'), - ), - undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('T'), - undefined, - ), - ), - ], - ), - ts.factory.createInterfaceDeclaration( - [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier('ILocale'), + ts.factory.createIdentifier('Locale'), undefined, undefined, - [ - ts.factory.createIndexSignature( + members, + ), + ts.factory.createVariableStatement( + [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], + ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration( + ts.factory.createIdentifier('locales'), undefined, - [ - ts.factory.createParameterDeclaration( + ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature( + undefined, + [ts.factory.createParameterDeclaration( undefined, undefined, - ts.factory.createIdentifier('_'), + ts.factory.createIdentifier('lang'), undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), undefined, - ), - ], - ts.factory.createUnionTypeNode([ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ParameterizedString'), - ), + )], ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ILocale'), + ts.factory.createIdentifier('Locale'), undefined, ), - ]), - ), - ], - ), - ts.factory.createInterfaceDeclaration( - [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier('Locale'), - undefined, - [ - ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ - ts.factory.createExpressionWithTypeArguments( - ts.factory.createIdentifier('ILocale'), - undefined, - ), - ]), - ], - members, - ), - ts.factory.createVariableStatement( - [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier('locales'), - undefined, - ts.factory.createTypeLiteralNode([ - ts.factory.createIndexSignature( - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('lang'), - undefined, - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.StringKeyword, - ), - undefined, - ), - ], - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('Locale'), - undefined, - ), - ), - ]), - undefined, - ), - ], - ts.NodeFlags.Const, + )]), + undefined, + )], + ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ), ), ts.factory.createFunctionDeclaration( @@ -192,39 +70,16 @@ export default function generateDTS() { ), ts.factory.createExportDefault(ts.factory.createIdentifier('locales')), ]; - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.MultiLineCommentTrivia, - ' eslint-disable ', - true, - ); - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.SingleLineCommentTrivia, - ' This file is generated by locales/generateDTS.js', - true, + const printed = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + }).printList( + ts.ListFormat.MultiLine, + ts.factory.createNodeArray(elements), + ts.createSourceFile('index.d.ts', '', ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS), ); - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.SingleLineCommentTrivia, - ' Do not edit this file directly.', - true, - ); - const printed = ts - .createPrinter({ - newLine: ts.NewLineKind.LineFeed, - }) - .printList( - ts.ListFormat.MultiLine, - ts.factory.createNodeArray(elements), - ts.createSourceFile( - 'index.d.ts', - '', - ts.ScriptTarget.ESNext, - true, - ts.ScriptKind.TS, - ), - ); - fs.writeFileSync(`${__dirname}/index.d.ts`, printed, 'utf-8'); + fs.writeFileSync(`${__dirname}/index.d.ts`, `/* eslint-disable */ +// This file is generated by locales/generateDTS.js +// Do not edit this file directly. +${printed}`, 'utf-8'); } diff --git a/locales/hr-HR.yml b/locales/hr-HR.yml index 881aa8464e..9cfebdd01a 100644 --- a/locales/hr-HR.yml +++ b/locales/hr-HR.yml @@ -3,4 +3,3 @@ _lang_: "japanski" ok: "OK" gotIt: "Razumijem" cancel: "otkazati" - diff --git a/locales/ht-HT.yml b/locales/ht-HT.yml index 1698c9f280..e3595c79b6 100644 --- a/locales/ht-HT.yml +++ b/locales/ht-HT.yml @@ -16,4 +16,3 @@ _2fa: renewTOTPCancel: "Sispann" _widgets: profile: "pwofil" - diff --git a/locales/hu-HU.yml b/locales/hu-HU.yml index 2f7006484a..023a91494d 100644 --- a/locales/hu-HU.yml +++ b/locales/hu-HU.yml @@ -102,4 +102,3 @@ _deck: _columns: notifications: "Értesítések" tl: "Idővonal" - diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 17e433dc07..da91c9cfd4 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -81,7 +81,7 @@ exportRequested: "Kamu telah meminta ekspor. Ini akan memakan waktu sesaat. Sete importRequested: "Kamu telah meminta impor. Ini akan memakan waktu sesaat." lists: "Daftar" noLists: "Kamu tidak memiliki daftar apapun" -note: "Catatan" +note: "Catat" notes: "Catatan" following: "Ikuti" followers: "Pengikut" @@ -108,14 +108,11 @@ enterEmoji: "Masukkan emoji" renote: "Renote" unrenote: "Hapus renote" renoted: "Telah direnote" -renotedToX: "{name} telah merenote" cantRenote: "Postingan ini tidak dapat direnote" cantReRenote: "Renote tidak dapat direnote" quote: "Kutip" inChannelRenote: "Hanya renote dalam kanal" inChannelQuote: "Hanya kutip dalam kanal" -renoteToChannel: "Renote ke kanal" -renoteToOtherChannel: "Renote ke kanal lainnya" pinnedNote: "Catatan yang disematkan" pinned: "Sematkan ke profil" you: "Kamu" @@ -128,12 +125,9 @@ emojiPicker: "Emoji Picker" pinnedEmojisForReactionSettingDescription: "Atur sematan emoji pada reaksi" pinnedEmojisSettingDescription: "Atur sematan emoji pada masukan emoji" emojiPickerDisplay: "Tampilan Emoji Picker" -overwriteFromPinnedEmojisForReaction: "Timpa dari pengaturan reaksi" -overwriteFromPinnedEmojis: "Timpa dari pengaturan umum" reactionSettingDescription2: "Geser untuk memindah urutan emoji, klik untuk menghapus, tekan \"+\" untuk menambahkan" rememberNoteVisibility: "Ingat pengaturan visibilitas catatan" attachCancel: "Hapus lampiran" -deleteFile: "Berkas dihapus" markAsSensitive: "Tandai sebagai konten sensitif" unmarkAsSensitive: "Hapus tanda konten sensitif" enterFileName: "Masukkan nama berkas" @@ -180,10 +174,6 @@ addAccount: "Tambahkan akun" reloadAccountsList: "Muat ulang daftar akun" loginFailed: "Gagal untuk masuk" showOnRemote: "Lihat profil asli" -continueOnRemote: "Lihat di peladen asal" -chooseServerOnMisskeyHub: "Pilih peladen dari Misskey Hub" -specifyServerHost: "Tentukan domain peladen" -inputHostName: "Masukkan nama domain" general: "Umum" wallpaper: "Wallpaper" setWallpaper: "Atur wallpaper" @@ -320,7 +310,6 @@ selectFile: "Pilih berkas" selectFiles: "Pilih berkas" selectFolder: "Pilih folder" selectFolders: "Pilih folder" -fileNotSelected: "Tidak ada file yang dipilih" renameFile: "Ubah nama berkas" folderName: "Nama folder" createFolder: "Buat folder" @@ -388,11 +377,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Nyalakan hCaptcha" hcaptchaSiteKey: "Site Key" hcaptchaSecretKey: "Secret Key" -mcaptcha: "mCaptcha" -enableMcaptcha: "Nyalakan mCaptcha" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret Key" -mcaptchaInstanceUrl: "URL instansi mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Nyalakan reCAPTCHA" recaptchaSiteKey: "Site key" @@ -408,7 +392,6 @@ name: "Nama" antennaSource: "Sumber Antenna" antennaKeywords: "Kata kunci yang diterima" antennaExcludeKeywords: "Kata kunci yang dikecualikan" -antennaExcludeBots: "Kecualikan akun bot" antennaKeywordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR." notifyAntenna: "Beritahu untuk catatan baru" withFileAntenna: "Hanya tampilkan catatan dengan berkas yang dilampirkan" @@ -485,7 +468,6 @@ noteOf: "Catatan milik {user}" inviteToGroup: "Undang ke grup" quoteAttached: "Dikutip" quoteQuestion: "Apakah kamu ingin menambahkan kutipan?" -attachAsFileQuestion: "Teks dalam papan klip terlalu panjang. Apakah kamu ingin melampirkannya sebagai berkas teks?" noMessagesYet: "Tidak ada pesan" newMessageExists: "Kamu mendapatkan pesan baru" onlyOneFileCanBeAttached: "Kamu hanya dapat melampirkan satu berkas ke dalam pesan" @@ -513,10 +495,9 @@ aboutX: "Tentang {x}" emojiStyle: "Gaya emoji" native: "Native" disableDrawer: "Jangan gunakan menu bergaya laci" +showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk" youHaveNoGroups: "Kamu tidak memiliki grup" joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri." -showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk" -showReactionsCount: "Lihat jumlah reaksi dalam catatan" noHistory: "Tidak ada riwayat" signinHistory: "Riwayat masuk" enableAdvancedMfm: "Nyalakan MFM tingkat lanjut" @@ -655,7 +636,6 @@ medium: "Sedang" small: "Kecil" generateAccessToken: "Buat token akses" permission: "Izin" -adminPermission: "Wewenang Izin Admin" enableAll: "Aktifkan semua" disableAll: "Nonaktifkan semua" tokenRequested: "Berikan ijin akses ke akun" @@ -699,7 +679,6 @@ useGlobalSettingDesc: "Jika dinyalakan, setelan notifikasi akun kamu akan diguna other: "Lainnya" regenerateLoginToken: "Perbarui token login" regenerateLoginTokenDescription: "Perbarui token yang digunakan secara internal saat login. Normalnya aksi ini tidak diperlukan. Jika diperbarui, semua perangkat akan dilogout." -theKeywordWhenSearchingForCustomEmoji: "Kata kunci ini digunakan untuk mencari emoji kustom yang dicari." setMultipleBySeparatingWithSpace: "Kamu dapat menyetel banyak dengan memisahkannya menggunakan spasi." fileIdOrUrl: "File-ID atau URL" behavior: "Perilaku" @@ -912,8 +891,6 @@ makeReactionsPublicDescription: "Pengaturan ini akan membuat daftar dari semua r classic: "Klasik" muteThread: "Bisukan thread" unmuteThread: "Suarakan thread" -followingVisibility: "Visibilitas mengikuti" -followersVisibility: "Visibilitas pengikut" continueThread: "Lihat lanjutan thread" deleteAccountConfirm: "Akun akan dihapus. Apakah kamu yakin?" incorrectPassword: "Kata sandi salah." @@ -1016,7 +993,6 @@ neverShow: "Jangan tampilkan lagi" remindMeLater: "Mungkin nanti" didYouLikeMisskey: "Apakah kamu mulai menyukai CherryPick?" pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu CherryPick. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan CherryPick tetap dapat berlanjut!" -correspondingSourceIsAvailable: "Sumber kode terkait tersedia di {anchor}" roles: "Peran" role: "Peran" noRole: "Peran tidak temukan" @@ -1067,9 +1043,6 @@ resetPasswordConfirm: "Yakin untuk mereset kata sandimu?" sensitiveWords: "Kata sensitif" sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru." sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." -prohibitedWords: "Kata yang dilarang" -prohibitedWordsDescription: "Menyalakan kesalahan ketika mencoba untuk memposting catatan dengan set kata-kata yang termasuk. Beberapa kata dapat diatur dan dipisahkan dengan baris baru." -prohibitedWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." hiddenTags: "Tagar tersembunyi" hiddenTagsDescription: "Pilih tanda yang mana akan tidak diperlihatkan dalam daftar tren.\nTanda lebih dari satu dapat didaftarkan dengan tiap baris." notesSearchNotAvailable: "Pencarian catatan tidak tersedia." @@ -1088,8 +1061,6 @@ limitWidthOfReaction: "Batasi lebar maksimum reaksi dan tampilkan dalam ukuran t noteIdOrUrl: "ID catatan atau URL" video: "Video" videos: "Video" -audio: "Suara" -audioFiles: "Berkas Suara" dataSaver: "Penghemat data" accountMigration: "Pemindahan akun" accountMoved: "Pengguna ini telah berpindah ke akun baru:" @@ -1185,7 +1156,6 @@ showRenotes: "Tampilkan renote" edited: "Telah disunting" notificationRecieveConfig: "Pengaturan notifikasi" mutualFollow: "Saling mengikuti" -followingOrFollower: "Mengikuti atau pengikut" fileAttachedOnly: "Hanya catatan dengan berkas" showRepliesToOthersInTimeline: "Tampilkan balasan ke pengguna lain dalam lini masa" hideRepliesToOthersInTimeline: "Sembunyikan balasan ke orang lain dari lini masa" @@ -1194,13 +1164,6 @@ hideRepliesToOthersInTimelineAll: "Sembuyikan balasan ke lainnya dari semua oran confirmShowRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menampilkan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?" confirmHideRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menyembunyikan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?" externalServices: "Layanan eksternal" -sourceCode: "Sumber kode" -sourceCodeIsNotYetProvided: "Sumber kode belum tersedia. Hubungi admin untuk memperbaiki masalah ini." -repositoryUrl: "URL Repositori" -repositoryUrlDescription: "Jika kamu menggunakan CherryPick begitu saja (tanpa ada perubahan dalam kode sumber), masukkan https://github.com/kokonect-link/cherrypick" -repositoryUrlOrTarballRequired: "Apabila kamu masih mempublikasikan repositori, kamu setidaknya harus menyediakan berkas tarball. Lihat .config/example.yml untuk informasi lebih lanjut." -feedback: "Umpan balik" -feedbackUrl: "URL Umpan balik" impressum: "Impressum" impressumUrl: "Tautan Impressum" impressumDescription: "Pada beberapa negara seperti Jerman, inklusi dari informasi kontak operator (sebuah Impressum) diperlukan secara legal untuk situs web komersil." @@ -1225,64 +1188,7 @@ doReaction: "Tambahkan reaksi" code: "Kode" reloadRequiredToApplySettings: "Muat ulang diperlukan untuk menerapkan pengaturan." remainingN: "Sisa : {n}" -overwriteContentConfirm: "Apakah kamu yakin untuk menimpa konten saat ini?" -seasonalScreenEffect: "Efek layar musiman" decorate: "Dekor" -addMfmFunction: "Tambahkan dekorasi" -enableQuickAddMfmFunction: "Tampilkan pemilih MFM tingkat lanjut" -bubbleGame: "Bubble Game" -sfx: "Efek Suara" -soundWillBePlayed: "Suara yang akan dimainkan" -showReplay: "Lihat tayangan ulang" -replay: "Tayangan ulang" -replaying: "Menayangkan Ulang" -endReplay: "Keluat dari tayangan ulang" -copyReplayData: "Salin data tayangan ulang" -ranking: "Peringkat" -lastNDays: "{n} hari terakhir" -backToTitle: "Ke Judul" -hemisphere: "Letak kamu tinggal" -withSensitive: "Lampirkan catatan dengan berkas sensitif" -userSaysSomethingSensitive: "Postingan oleh {name} mengandung konten sensitif" -enableHorizontalSwipe: "Geser untuk mengganti tab" -loading: "Memuat..." -surrender: "Batalkan" -gameRetry: "Coba lagi" -notUsePleaseLeaveBlank: "Kosongi bila tidak digunakan" -useTotp: "Gunakan TOTP" -useBackupCode: "Gunakan kode cadangan" -launchApp: "Luncurkan Aplikasi" -useNativeUIForVideoAudioPlayer: "Gunakan antarmuka peramban ketika memainkan video dan audio" -keepOriginalFilename: "Simpan nama berkas asli" -keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas akan diganti dengan string acak secara otomatis ketika kamu mengunggah berkas." -noDescription: "Tidak ada deskripsi" -alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti" -inquiry: "Hubungi kami" -tryAgain: "Silahkan coba lagi." -_delivery: - status: "Status pengiriman" - stop: "Ditangguhkan" - resume: "Lanjutkan pengiriman" - _type: - none: "Sedang menyiarkan langsung" - manuallySuspended: "Ditangguhkan manual" - goneSuspended: "Sedang ditangguhkan untuk penghapusan peladen" - autoSuspendedForNotResponding: "Sedang ditangguhkan karena peladen tidak menjawab" -_bubbleGame: - howToPlay: "Cara bermain" - hold: "Tahan" - _score: - score: "Skor" - scoreYen: "Jumlah uang didapat" - highScore: "Skor tertinggi" - maxChain: "Jumlah skor berantai" - yen: "{yen} Yen" - estimatedQty: "{qty} buah" - scoreSweets: "{onigiriQtyWithUnit} onigiri" - _howToPlay: - section1: "Atur posisi dan jatuhkan obyek ke dalam kotak." - section2: "Ketika dua obyek menyentuh tipe yang sama satu sama lain, obyek tersebut akan berganti dan kamu mendapatkan poin skor." - section3: "Permainan berakhir jika obyek memenuhi kotak. Capai skor tertinggi dengan menggabungkan obyek bersama sambil menghindari obyek tersebut memenuhi kotak permainan!" _announcement: forExistingUsers: "Hanya pengguna yang telah ada" forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya." @@ -1292,10 +1198,7 @@ _announcement: tooManyActiveAnnouncementDescription: "Terlalu banyak pengumuman dapat memperburuk pengalaman pengguna. Mohon pertimbangkan untuk mengarsipkan pengumuman yang sudah usang/tidak relevan." readConfirmTitle: "Tandai telah dibaca?" readConfirmText: "Aksi ini akan menandai konten dari \"{title}\" telah dibaca." - shouldNotBeUsedToPresentPermanentInfo: "Karena dapat berdampak pada pengalaman pengguna untuk pengguna baru, sangat direkomendasikan untuk menggunakan notifikasi secara mengalir daripada tetap." - dialogAnnouncementUxWarn: "Memiliki dua atau lebih gaya dialog notifikasi secara bersamaan dapat berdampak signifikan pada pengalaman pengguna, mohon untuk menggunakannya dengan hati-hati." silence: "Tiada notifikasi" - silenceDescription: "Apabila diaktifkan, notifikasi dari pengumuman ini akan dilewatkan dan pengguna tidak perlu membacanya." _initialAccountSetting: accountCreated: "Akun kamu telah sukses dibuat!" letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu." @@ -1308,7 +1211,6 @@ _initialAccountSetting: pushNotificationDescription: "Menyalakan notifikasi dorong akan membuatmu menerima notifikasi dari {name} secara langsung ke perangkatmu." initialAccountSettingCompleted: "Pengaturan profil selesai!" haveFun: "Selamat menikmati, {name}!" - youCanContinueTutorial: "Kamu dapat menjutkan ke tutorial dalam bagaimana menggunakan {name} (CherryPick) atau kamu dapat keluar dari pemasangan ini dan langsung menggunakannya segera." startTutorial: "Mulai Tutorial" skipAreYouSure: "Yakin melewati atur profil?" laterAreYouSure: "Yakin banget untuk atur profil nanti?" @@ -1322,63 +1224,24 @@ _initialTutorial: description: "Di sini kamu dapat mempelajari dasar-dasar dari penggunaan CherryPick dan fitur-fiturnya." _note: title: "Apa itu Catatan?" - description: "Postingan di CherryPick disebut sebagai 'Catatan'. Catatan ditampilkan secara kronologis pada lini masa dan dimutakhirkan secara real-time." - reply: "Klik pada tombol ini untuk membalas ke sebuah pesan. Bisa juga untuk membalas ke sebuah balasan dan melanjutkannya seperti percakapan selayaknya utas." - renote: "Kamu dapat membagikan catatan ke lini masa milikmu. Kamu juga dapat mengutipnya dengan komentarmu." - reaction: "Kamu dapat menambahkan reaksi ke Catatan. Detil lebih lanjut akan dijelaskan di halaman berikutnya." - menu: "Kamu dapat melihat detil catatan, menyalin tautan, dan melakukan aksi lainnya." _reaction: title: "Apa itu Reaksi?" - description: "Catatan dapat direaksi dengan berbagai emoji. Reaksi memperbolehkan kamu untuk mengekspresikan nuansa yang tidak dapat disampaikan hanya dengan sebuah \"suka\"." - letsTryReacting: "Reaksi dapat ditambahkan dengan mengklik tombol '+' pada catatan. Coba lakukan mereaksi contoh catatan ini!" - reactToContinue: "Tambahkan reaksi untuk melanjutkan." - reactNotification: "Kamu akan menerima notifikasi real0time ketika seseorang mereaksi catatan kamu." - reactDone: "Kamu dapat mengurungkan reaksi dengan menekan tombol '-'." _timeline: title: "Konsep Lini Masa" - description1: "CherryPick menyediakan berbagai lini masa sesuai dengan penggunaan (beberapa mungkin tidak tersedia karena bergantung dengan kebijakan peladen)." - home: "Kamu dapat melihat catatan dari akun yang kamu ikuti." - local: "Kamu dapat melihat catatan dari semua pengguna yang ada pada peladen ini." - social: "Catatan dari linimasa Beranda dan Lokal akan ditampilkan." - global: "Kamu dapat melihat catatan dari semua peladen yang terhubung." - description2: "Kamu dapat mengganti linimasa di bagian atas layar kamu kapan saja." - description3: "Sebagai tambahan, terdapat juga linimasa daftar dan linimasa kanal. Untuk detil lebih lanjut, silahkan melihat ke tautan berikut: {link}." _postNote: title: "Pengaturan posting Catatan" - description1: "Ketika memposting catatan ke CherryPick, terdapat beberapa opsi yang tersedia. Form posting terlihat seperti ini." _visibility: - description: "Kamu dapat membatasi siapa yang dapat melihat catatan kamu." public: "Perlihatkan catatan ke semua pengguna." home: "Hanya publik ke lini masa Beranda. Pengguna yang mengunjungi profilmu melalui pengikut dan renote dapat melihatnya." followers: "Perlihatkan ke pengikut saja. Hanya pengikut yang dapat melihat postinganmu dan tidak dapat direnote oleh siapapun." direct: "Hanya perlihatkan ke pengguna spesifik dan penerima akan diberi tahu. Dapat juga digunakan sebagai alternatif dari pesan langsung." - doNotSendConfidencialOnDirect1: "Hati-hati ketika mengirim informasi yang sensitif!" - doNotSendConfidencialOnDirect2: "Admin dari peladen dapat melihat apa yang kamu tulis. Hati-hati dengan informasi sensitif ketika mengirimkan catatan langsung kepada pengguna pada peladen yang tidak dipercaya." - localOnly: "Memposting dengan opsi ini tidak akan memfederasi catatan ke peladen lain. Pengguna pada peladen lain tidak akan dapat melihat catatan ini secara langsung, meskipun dengan pengaturan visibilitas yang sudah diatur di atas." _cw: title: "Peringatan Konten (CW)" - description: "Alih-alih isinya, konten yang ditulis dalam kolom 'komentar' akan ditampilkan. Menekan 'Selebihnya' akan menampilkan isi konten." _exampleNote: cw: "Peringatan: Bikin Lapar!" note: "Baru aja makan donat berlapis coklat 🍩😋" - useCases: "Fungsi ini digunakan ketika mengikutik panduan peladen untuk catatan yang dibutuhkan atau untuk membatasi diri dari teks sensitif atau spoiler." _howToMakeAttachmentsSensitive: title: "Bagaimana menandai lampiran sebagai sensitif?" - description: "Fungsi ini digunakan untuk lampiran yang dibutuhkan oleh panduan peladen atau sesuatu yang seharusnya tidak boleh dibiarkan begitu saja dengan cara menambahkan penanda \"sensitif\"." - tryThisFile: "Coba tandai gambar yang dilampirkan pada form ini sebagai sensitif!" - _exampleNote: - note: "Ups, kesalahan banget buka penutup wadah natto..." - method: "Untuk menandai lampiran sebagai sensitif, klik gambar pada berkas, buka menu, lalu klik \"Tandai sebagai sensitif\"." - sensitiveSucceeded: "Ketika melampirkan berkas, mohon atur sensitifitas sesuai dengan panduan peladen." - doItToContinue: "Tandai berkas terlampir sebagai sensitif untuk melanjutkan." - _done: - title: "Kamu telah menyelesaikan tutorial! 🎉" - description: "Fungsi yang diperkenalkan di sini merupakan sebagian kecil dari fitur yang ada. Untuk pemahaman lebih detil dalam menggunakan CherryPick, kamu dapat merujuk ke {link}." -_timelineDescription: - home: "Pada linimasa Beranda, kamu dapat melihat catatan dari akun yang kamu ikuti." - local: "Pada linimasa Lokal, kamu dapat melihat catatan dari semua pengguna yang ada pada peladen ini." - social: "Linimasa sosial menampilkan catatan dari kedua linimasa Beranda dan Lokal." - global: "Pada linimasa Global, kamu dapat melihat catatan dari semua peladen yang terhubung." _serverRules: description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan." _serverSettings: @@ -1390,9 +1253,6 @@ _serverSettings: manifestJsonOverride: "Ambil alih manifest.json" shortName: "Nama pendek" shortNameDescription: "Inisial untuk nama instansi yang dapat ditampilkan apabila nama lengkap resmi terlalu panjang." - fanoutTimelineDescription: "Dapat meningkatkan performa dalam pengambilan data linimasa dan mengurangi beban pada database ketika dinyalakan. Sebagai gantinya, penggunaan memory pada Redis akan meningkan. Pertimbangkan untuk menonaktifkan fitur ini jika mengalami kekurangan memori pada server atau menyebabkan server tidak stabil." - fanoutTimelineDbFallback: "Fallback ke database" - fanoutTimelineDbFallbackDescription: "Ketika diaktifkan, lini masa akan fallback ke database untuk melakukan kueri tambahan apabila linimasa tidak disimpan dalam cache. Menonaktifkan ini dapat mengurangi beban server dengan mengeliminasi proses fallback, namun dapat berakibat membatasi jarak data dari lini masa yang dapat diambil." _accountMigration: moveFrom: "Pindahkan akun lain ke akun ini" moveFromSub: "Buat alias ke akun lain" @@ -1464,7 +1324,7 @@ _achievements: _login3: title: "Pemula I" description: "Login selama 3 hari" - flavor: "Mulai hari ini, panggil gue Cherrypikist" + flavor: "Mulai hari ini, panggil gue Misskist" _login7: title: "Pemula II" description: "Login selama 7 hari" @@ -1569,7 +1429,7 @@ _achievements: _viewAchievements3min: title: "Suka Pencapaian" description: "Lugat daftar pencapaianmu setidaknya 3 menit" - _iLoveCherryPick: + _iLoveMisskey: title: "I Love CherryPick" description: "Catat \"I ❤ #CherryPick\"" flavor: "Tim pengembang cherrypick sangat mengapresiasi dukungan kamu!" @@ -1653,16 +1513,6 @@ _achievements: _smashTestNotificationButton: title: "Tes overflow" description: "Picu tes notifikasi secara berulang dalam waktu yang sangat pendek" - _tutorialCompleted: - title: "Ijazah Sekolah Dasar CherryPick" - description: "Tutorial selesai" - _bubbleGameExplodingHead: - title: "🤯" - description: "Obyek paling terbesar di permainan gelembung" - _bubbleGameDoubleExplodingHead: - title: "Ganda 🤯" - description: "Dua dari obyek paling terbesar pada permainan gelembung di waktu yang sama" - flavor: "Kamu dapat mengisi kotak makan siang seperti ini 🤯 🤯." _role: new: "Buat peran" edit: "Sunting peran" @@ -1673,9 +1523,7 @@ _role: assignTarget: "Tipe tugas" descriptionOfAssignTarget: "Manual untuk mengganti secara manual siapa yang mendapatkan peran ini dan siapa yang tidak.\nKondisional untuk pengguna secara otomatis dimasukkan atau dihapus dari peran berdasarkan kondisi yang ditentukan." manual: "Manual" - manualRoles: "Peran manual" conditional: "Kondisional" - conditionalRoles: "Peran kondisional" condition: "Kondisi" isConditionalRole: "Ini adalah peran kondisional" isPublic: "Publikkan Peran" @@ -1703,7 +1551,6 @@ _role: gtlAvailable: "Dapat melihat lini masa global" ltlAvailable: "Dapat melihat lini masa lokal" canPublicNote: "Dapat mengirim catatan publik" - mentionMax: "Jumlah maksimum sebutan dalam sebuah catatan" canInvite: "Dapat membuat kode undangan instansi" inviteLimit: "Batas jumlah undangan" inviteLimitCycle: "Interval Penerbitan Kode Undangan" @@ -1725,16 +1572,9 @@ _role: canHideAds: "Dapat menyembunyikan iklan" canSearchNotes: "Penggunaan pencarian catatan" canUseTranslator: "Penggunaan penerjemah" - avatarDecorationLimit: "Jumlah maksimum dekorasi avatar yang dapat diterapkan" _condition: - roleAssignedTo: "Ditugaskan ke peran manual" isLocal: "Pengguna lokal" isRemote: "Pengguna remote" - isCat: "Pengguna Kucing" - isBot: "Pengguna Bot" - isSuspended: "Pengguna yang ditangguhkan" - isLocked: "Akun privat" - isExplorable: "Pengguna efektif yang akunnya dapat dicari" createdLessThan: "Telah berlalu kurang dari X sejak pembuatan akun" createdMoreThan: "Telah berlalu lebih dari X sejak pembuatan akun" followersLessThanOrEq: "Memiliki pengikut X atau kurang dari tersebut" @@ -1760,9 +1600,8 @@ _emailUnavailable: disposable: "Alamat surel temporer tidak dapat digunakan" mx: "Peladen alamat surel ini tidak valid" smtp: "Peladen alamat surel ini tidak merespon" - banned: "Kamu tidak dapat mendaftar dengan alamat surel ini" _ffVisibility: - public: "Publik" + public: "Terbitkan" followers: "Tampil untuk pengikut saja" private: "Tersembunyi" _signup: @@ -1804,7 +1643,6 @@ _plugin: installWarn: "Mohon jangan memasang plugin yang tidak dapat dipercayai." manage: "Manajemen plugin" viewSource: "Lihat sumber" - viewLog: "Tampilkan log" _preferencesBackups: list: "Cadangan yang dibuat" saveNew: "Simpan cadangan baru" @@ -1834,13 +1672,10 @@ _aboutMisskey: contributors: "Kontributor utama" allContributors: "Seluruh kontributor" source: "Sumber kode" - original: "Asli" - thisIsModifiedVersion: "{name} menggunakan versi modifikasi dari CherryPick yang asli." translation: "Terjemahkan Misskey" donate: "Donasi ke Misskey" morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang tidak tercantum disini. Terima kasih! 🥰" patrons: "Pendukung" - projectMembers: "Anggota proyek" _displayOfSensitiveMedia: respect: "Sembunyikan media yang ditandai sensitif" ignore: "Tampilkan media yang ditandai sensitif" @@ -1929,7 +1764,6 @@ _channel: notesCount: "terdapat {n} catatan" nameAndDescription: "Nama dan deskripsi" nameOnly: "Hanya nama" - allowRenoteToExternal: "Perbolehkan catat ulang dan kutipan di luar dari kanal" _menuDisplay: sideFull: "Horisontal" sideIcon: "Horisontal (Ikon)" @@ -2021,14 +1855,8 @@ _sfx: notification: "Notifikasi" chat: "Pesan" chatBg: "Obrolan (Latar Belakang)" - reaction: "Ketika memilih reaksi" -_soundSettings: - driveFile: "Menggunakan berkas audio dalam Drive" - driveFileWarn: "Pilih berkas audio dari Drive" - driveFileTypeWarn: "Berkas ini tidak didukung" - driveFileTypeWarnDescription: "Pilih berkas audio" - driveFileDurationWarn: "Audio ini terlalu panjang" - driveFileDurationWarnDescription: "Audio panjang dapat mengganggu penggunaan CherryPick. Masih ingin melanjutkan?" + antenna: "Penerimaan Antenna" + channel: "Notifikasi Kanal" _ago: future: "Masa depan" justNow: "Baru saja" @@ -2040,14 +1868,6 @@ _ago: monthsAgo: "{n} bulan lalu" yearsAgo: "{n} tahun lalu" invalid: "Tidak ada sama sekali disini" -_timeIn: - seconds: "dalam {n} detik" - minutes: "dalam {n} menit" - hours: "dalam {n} jam" - days: "dalam {n} hari" - weeks: "dalam {n} minggu" - months: "dalam {n} bulan" - years: "dalam {n} tahun" _time: second: "detik" minute: "menit" @@ -2058,6 +1878,7 @@ _2fa: registerTOTP: "Daftarkan aplikasi autentikator" step1: "Pertama, pasang aplikasi autentikasi (seperti {a} atau {b}) di perangkat kamu." step2: "Lalu, pindai kode QR yang ada di layar." + step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel." step2Uri: "Masukkan URI berikut jika kamu menggunakan program desktop" step3Title: "Masukkan kode autentikasi" step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." @@ -2081,7 +1902,6 @@ _2fa: backupCodesDescription: "Kamu dapat menggunakan kode ini untuk mendapatkan akses ke akun kamu apabila berada dalam situasi tidak dapat menggunakan aplikasi autentikasi 2-faktor yang kamu miliki. Setiap kode hanya dapat digunakan satu kali. Mohon simpan kode ini di tempat yang aman." backupCodeUsedWarning: "Kode cadangan telah digunakan. Mohon mengatur ulang autentikasi 2-faktor secepatnya apabila kamu sudah tidak dapat menggunakannya lagi." backupCodesExhaustedWarning: "Semua kode cadangan telah digunakan. Apabila kamu kehilangan akses pada aplikasi autentikasi 2-faktor milikmu, kamu tidak dapat mengakses akun ini lagi. Mohon atur ulang autentikasi 2-faktor kamu." - moreDetailedGuideHere: "Berikut panduan detilnya" _permissions: "read:account": "Lihat informasi akun" "write:account": "Sunting informasi akun" @@ -2119,54 +1939,6 @@ _permissions: "write:flash": "Sunting Play" "read:flash-likes": "Lihat daftar Play yang disukai" "write:flash-likes": "Sunting daftar Play yang disukai" - "read:admin:abuse-user-reports": "Lihat laporan pengguna" - "write:admin:delete-account": "Hapus akun pengguna" - "write:admin:delete-all-files-of-a-user": "Hapus semua berkas dari seorang pengguna" - "read:admin:index-stats": "Lihat statistik indeks basis data" - "read:admin:table-stats": "Lihat statistik tabel basis data" - "read:admin:user-ips": "Lihat alamat IP pengguna" - "read:admin:meta": "Lihat metadata instansi" - "write:admin:reset-password": "Atur ulang kata sandi pengguna" - "write:admin:resolve-abuse-user-report": "Selesaikan laporan pengguna" - "write:admin:send-email": "Mengirim surel" - "read:admin:server-info": "Lihat informasi peladen" - "read:admin:show-moderation-log": "Lihat log moderasi" - "read:admin:show-user": "Lihat informasi pengguna privat" - "write:admin:suspend-user": "Tangguhkan pengguna" - "write:admin:unset-user-avatar": "Hapus avatar pengguna" - "write:admin:unset-user-banner": "Hapus banner pengguna" - "write:admin:unsuspend-user": "Batalkan penangguhan pengguna" - "write:admin:meta": "Kelola metadata instansi" - "write:admin:user-note": "Kelola moderasi catatan" - "write:admin:roles": "Kelola peran" - "read:admin:roles": "Lihat peran" - "write:admin:relays": "Kelola relay" - "read:admin:relays": "Lihat relay" - "write:admin:invite-codes": "Kelola kode undangan" - "read:admin:invite-codes": "Lihat kode undangan" - "write:admin:announcements": "Kelola pengumuman" - "read:admin:announcements": "Lihat Pengumuman" - "write:admin:avatar-decorations": "Kelola dekorasi avatar" - "read:admin:avatar-decorations": "Lihat dekorasi avatar" - "write:admin:federation": "Kelola data federasi" - "write:admin:account": "Kelola akun pengguna" - "read:admin:account": "Lihat akun pengguna" - "write:admin:emoji": "Kelola emoji" - "read:admin:emoji": "Lihat emoji" - "write:admin:queue": "Kelola antrian kerja" - "read:admin:queue": "Lihat informasi antrian kerja" - "write:admin:promo": "Kelola catatan promosi" - "write:admin:drive": "Kelola drive pengguna" - "read:admin:drive": "Kelola informasi drive pengguna" - "read:admin:stream": "Gunakan API WebSocket untuk Admin" - "write:admin:ad": "Kelola iklan" - "read:admin:ad": "Lihat iklan" - "write:invite-codes": "Membuat kode undangan" - "read:invite-codes": "Mendapatkan kode undangan" - "write:clip-favorite": "Kelola klip yang difavoritkan" - "read:clip-favorite": "Lihat klip yang difavoritkan" - "read:federation": "Mendapatkan data federasi" - "write:report-abuse": "Melaporkan pelanggaran" _auth: shareAccessTitle: "Mendapatkan ijin akses aplikasi" shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?" @@ -2222,7 +1994,6 @@ _widgets: _userList: chooseList: "Pilih daftar" clicker: "Pengeklik" - birthdayFollowings: "Pengguna yang merayakan hari ulang tahunnya hari ini" _cw: hide: "Sembunyikan" show: "Lihat konten" @@ -2285,11 +2056,9 @@ _profile: changeAvatar: "Ubah avatar" changeBanner: "Ubah header" verifiedLinkDescription: "Dengan memasukkan URL yang mengandung tautan ke profil kamu di sini, ikon verifikasi kepemilikan dapat ditampilkan di sebelah kolom ini." - avatarDecorationMax: "Dapat ditambahkan hingga {max} dekorasi." _exportOrImport: allNotes: "Semua catatan" favoritedNotes: "Catatan favorit" - clips: "Klip" followingList: "Ikuti" muteList: "Bisukan" blockingList: "Blokir" @@ -2344,7 +2113,6 @@ _play: title: "Judul" script: "Script" summary: "Deskripsi" - visibilityDescription: "Membuat catatan ini privat berarti tidak akan terlihat pada profil kamu, namun siapapun yang memiliki URL dari catatan ini akan dapat mengaksesnya." _pages: newPage: "Buat halaman baru" editPage: "Sunting halaman" @@ -2389,8 +2157,6 @@ _pages: section: "Bagian" image: "Gambar" button: "Tombol" - dynamic: "Blok Dinamis" - dynamicDescription: "Blok ini telah dihapus. Mohon gunakan {play} dari sekarang." note: "Catatan yang ditanam" _note: id: "ID Catatan" @@ -2413,18 +2179,12 @@ _notification: pollEnded: "Hasil Kuesioner telah keluar" newNote: "Catatan baru" unreadAntennaNote: "Antena {name}" - roleAssigned: "Peran Diberikan" emptyPushNotificationMessage: "Pembaruan notifikasi dorong" achievementEarned: "Pencapaian didapatkan" testNotification: "Tes notifikasi" checkNotificationBehavior: "Cek tampilan notifikasi" sendTestNotification: "Kirim tes notifikasi" notificationWillBeDisplayedLikeThis: "Notifikasi akan terlihat seperti ini" - reactedBySomeUsers: "{n} orang memberikan reaksi" - likedBySomeUsers: "{n} pengguna menyukai catatan kamu" - renotedBySomeUsers: "{n} orang telah merenote" - followedBySomeUsers: "{n} orang telah mengikuti" - flushNotification: "Bersihkan notifikasi" _types: all: "Semua" note: "Catatan baru" @@ -2437,7 +2197,6 @@ _notification: pollEnded: "Jajak pendapat berakhir" receiveFollowRequest: "Permintaan mengikuti diterima" followRequestAccepted: "Permintaan mengikuti disetujui" - roleAssigned: "Peran Diberikan" achievementEarned: "Pencapaian didapatkan" groupInvited: "Diundang ke grup" app: "Notifikasi dari aplikasi tertaut" @@ -2487,9 +2246,9 @@ _drivecleaner: orderByCreatedAtAsc: "Tanggal (Naik)" _webhookSettings: createWebhook: "Buat Webhook" - modifyWebhook: "Sunting Webhook" name: "Nama" secret: "Secret" + events: "Webhook Events" active: "Aktif" _events: follow: "Ketika mengikuti pengguna" @@ -2499,11 +2258,6 @@ _webhookSettings: renote: "Ketika direnote" reaction: "Ketika menerima reaksi" mention: "Ketika sedang disebut" - deleteConfirm: "Apakah kamu yakin ingin menghapus Webhook?" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Surel" _moderationLogTypes: createRole: "Peran telah dibuat" deleteRole: "Peran telah dihapus" @@ -2528,7 +2282,6 @@ _moderationLogTypes: resetPassword: "Atur ulang kata sandi" suspendRemoteInstance: "Instansi luar telah ditangguhkan" unsuspendRemoteInstance: "Instansi luar batal ditangguhkan" - updateRemoteInstanceNote: "Catatan moderasi telah diperbaharui untuk peladen luar." markSensitiveDriveFile: "Berkas ditandai sensitif" unmarkSensitiveDriveFile: "Berkas batal ditandai sensitif" resolveAbuseReport: "Laporan terselesaikan" @@ -2536,11 +2289,6 @@ _moderationLogTypes: createAd: "Iklan telah dibuat" deleteAd: "Iklan telah dihapus" updateAd: "Iklan telah diperbaharui" - createAvatarDecoration: "Buat dekorasi avatar" - updateAvatarDecoration: "Perbarui dekorasi avatar" - deleteAvatarDecoration: "Hapus dekorasi avatar" - unsetUserAvatar: "Hapus avatar pengguna" - unsetUserBanner: "Hapus banner pengguna" _fileViewer: title: "Rincian berkas" type: "Jenis berkas" @@ -2549,126 +2297,3 @@ _fileViewer: uploadedAt: "Diunggah pada" attachedNotes: "Catatan yang dilampirkan" thisPageCanBeSeenFromTheAuthor: "Halaman ini hanya dapat dilihat oleh pengguna yang mengunggah bekas ini." -_externalResourceInstaller: - title: "Pasang dari situs eksternal" - checkVendorBeforeInstall: "Pastikan sumber dari sumber daya ini terpercaya sebelum melakukan pemasangan." - _plugin: - title: "Apakah kamu ingin memasang plugin ini?" - metaTitle: "Informasi plugin" - _theme: - title: "Apakah kamu ingin memasang tema ini?" - metaTitle: "Informasi tema" - _meta: - base: "Skema warna dasar" - _vendorInfo: - title: "Informasi sumber" - endpoint: "Referensi Endpoint" - hashVerify: "Verifikasi hash" - _errors: - _invalidParams: - title: "Parameter tidak valid" - description: "Tidak cukup informasi untuk memuat data dari situs eksternal. Mohon konfirmasi kembali URL yang dimasukkan." - _resourceTypeNotSupported: - title: "Sumber daya eksternal ini tidak didukung" - description: "Tipe sumber daya eksternal ini tidak didukung. Mohon kontak administrator dari situs tersebut." - _failedToFetch: - title: "Gagal memuat data" - fetchErrorDescription: "Kesalahan terjadi ketika menghubungkan dengan situs eksternal. Jika percobaan kembali tidak dapat memperbaiki masalah ini, mohon hubungi administrator dari situs tersebut." - parseErrorDescription: "Kesalahan terjadi dalam memproses data yang dimuat dari situs eksternal. Mohon hubungi administrator dari situs tersebut." - _hashUnmatched: - title: "Verifikasi data gagal" - description: "Kesalahan terjadi dalam memverifikasi integritas data yang diambil. Sebagai pencegahan keamanan, pemasangan tidak dapat dilanjutkan. Mohon hubungi administrator dari situs tersebut." - _pluginParseFailed: - title: "Kesalahan AiScript" - description: "Data yang diminta telah diambil dengan sukses, namun kesalahan terjadi ketika AiScript melakukan parsing. Mohon hubungi pembuat plugin. Detil kesalahan dapat dilihat pada konsol Javascript." - _pluginInstallFailed: - title: "Pemasangan plugin gagal" - description: "Kesalahan terjadi ketika pemasangan plugin. Mohon coba lagi. Detil kesalahan dapat dilihat pada konsol Javascript." - _themeParseFailed: - title: "Parsing tema gagal" - description: "Data yang diminta telah diambil dengan sukses, namun kesalahan terjadi ketika tema melakukan parsing. Mohon hubungi pembuat tema. Detil kesalahan dapat dilihat pada konsol Javascript." - _themeInstallFailed: - title: "Pemasangan tema gagal" - description: "Kesalahan terjadi ketika pemasangan tema. Mohon coba lagi. Detil kesalahan dapat dilihat pada konsol Javascript." -_dataSaver: - _media: - title: "Memuat media" - description: "Mencegah gambar/video dimuat secara otomatis. Menyembunyikan gambar/video dan akan dimuat ketika diketuk." - _avatar: - title: "Gambar avatar" - description: "Hentikan animasi gambar avatar. Gambar animasi dapat berukuran lebih besar dari gambar biasa, berpotensi pada pengurangan lalu lintas data lebih jauh." - _urlPreview: - title: "Gambar kecil URL pratinjau" - description: "Gambar kecil URL pratinjau tidak akan dimuat lagi." - _code: - title: "Penyorotan kode" - description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data." -_hemisphere: - N: "Bumi belahan utara" - S: "Bumi belahan selatan" - caption: "Digunakan dalam beberapa pengaturan klien untuk menentukan musim." -_reversi: - reversi: "Reversi" - gameSettings: "Pengaturan permainan" - chooseBoard: "Pilih papan" - blackOrWhite: "Hitam/Putih" - blackIs: "{name} bermain sebagai Hitam" - rules: "Aturan" - thisGameIsStartedSoon: "Permainan akan segera dimulai" - waitingForOther: "Menunggu langkah giliran dari lawan" - waitingForMe: "Menungguh langkah giliran dari kamu" - waitingBoth: "Bersiap" - ready: "Siap" - cancelReady: "Belum siap" - opponentTurn: "Giliran lawan" - myTurn: "Giliran kamu" - turnOf: "Giliran {name}" - pastTurnOf: "Giliran {name}" - surrender: "Menyerah" - surrendered: "Telah menyerah" - timeout: "Waktu habis" - drawn: "Seri" - won: "{name} menang" - black: "Hitam" - white: "Putih" - total: "Jumlah" - turnCount: "Langkah ke {count}" - myGames: "Rondeku" - allGames: "Semua ronde" - ended: "Selesai" - playing: "Sedang bermain" - isLlotheo: "Pemain dengan batu yang sedikit menang (Llotheo)" - loopedMap: "Peta melingkar" - canPutEverywhere: "Keping dapat ditaruh dimana saja" - timeLimitForEachTurn: "Batas waktu untuk gantian" - freeMatch: "Pertandingan bebas" - lookingForPlayer: "Mencari lawan..." - gameCanceled: "Permainan ini telah dibatalkan." - shareToTlTheGameWhenStart: "Bagikan permainan ke lini masa ketika dimulai" - iStartedAGame: "Permainan telah dimulai! #MisskeyReversi" - opponentHasSettingsChanged: "Lawan telah mengganti pengaturan mereka." - allowIrregularRules: "Aturan non-reguler (bebas sepenuhnya)" - disallowIrregularRules: "Tanpa aturan non-reguler" - showBoardLabels: "Tampilkan penomoran baris dan kolom pada papan" - useAvatarAsStone: "Ubah batu menjadi avatar pengguna" -_offlineScreen: - title: "Luring - tidak dapat terhubung ke peladen" - header: "Tidak dapat tersambung ke server" -_urlPreviewSetting: - title: "Pengaturan pratinjau URL" - enable: "Aktifkan pratinjau URL" - timeout: "Waktu timeout pratinjau URL (ms)" - timeoutDescription: "Apabila ini memakan waktu lama dari nilai yang ditentukan untuk mendapatkan pratinjau, pratinjau tidak akan dibuat." - maximumContentLength: "Content-Length Maksimum (bytes)" - maximumContentLengthDescription: "Apabila Content-Length lebih besar dari nilai ini, pratinjau tidak akan dibuat." - requireContentLength: "Buat pratinjau hanya ketika Content-Length dapat didapatkan" - requireContentLengthDescription: "Apabila peladen lain tidak memberika Content-Length, pratinjau tidak akan dibuat." - userAgent: "User-Agent" - userAgentDescription: "Atur User-Agent yang digunakan untuk mengambil pratinjau. Apabila dibiarkan kosong, User-Agent bawaan akan digunakan." - summaryProxy: "Titik akhir proksi yang membuat pratinjau" - summaryProxyDescription: "Bukan untuk CherryPick, namun untuk menghasilkan pratinjau menggunakan Summaly Proxy." - summaryProxyDescription2: "Parameter berikut tertautkan dengan proksi sebagai string kueri. Apabila proksi tidak mendukung tersebut, nilai di dalamnya diabaikan." -_mediaControls: - pip: "Gambar dalam Gambar" - playbackRate: "Kecepatan Pemutaran" - loop: "Ulangi Pemutaran" diff --git a/locales/index.d.ts b/locales/index.d.ts index 90a3be076b..0610089b7a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1,11816 +1,2950 @@ /* eslint-disable */ // This file is generated by locales/generateDTS.js // Do not edit this file directly. -declare const kParameters: unique symbol; -export interface ParameterizedString { - [kParameters]: T; -} -export interface ILocale { - [_: string]: string | ParameterizedString | ILocale; -} -export interface Locale extends ILocale { - /** - * 日本語 - */ +export interface Locale { "_lang_": string; - /** - * リノートの公開範囲を指定 - */ - "forceRenoteVisibilitySelector": string; - /** - * CherryPick研究室 - */ "cherrypickLabs": string; - /** - * まだ開発中の機能を試してみませんか。一部の機能はちゃんと動かないかもしれません。 - */ "cherrypickLabsDescription": string; - /** - * リンクをコピーしました! - */ "copiedLink": string; - /** - * 内容をコピーしました! - */ "copiedContent": string; - /** - * コピーしました! - */ "copied": string; - /** - * ようこそ! - */ "welcome": string; - /** - * CherryPickへの移行が完了しました! - */ "cherrypickMigrated": string; - /** - * キャッシュクリアのご案内 - */ "cherrypickMigratedCacheClearTitle": string; - /** - * このサーバーはMisskeyまたはCherryPick v4.3.0以前のバージョンから移行されました。 - * バージョン管理方式が異なり、残っているキャッシュが問題を引き起こす可能性があるため、移行後、最初の接続時にキャッシュを削除する作業を行う必要があります。 - * - * この作業は最初一度だけ行われます。 - */ "cherrypickMigratedCacheClear": string; - /** - * リノートの公開範囲オプションを表示 - */ "showRenoteVisibilitySelector": string; - /** - * この機能は現在使用できません。 - */ "cannotBeUsedFunc": string; - /** - * 大きさ - */ "scale": string; - /** - * 不透明度 - */ "opacity": string; - /** - * 編集済み: {date} {time} - */ - "noteUpdatedAt": ParameterizedString<"date" | "time">; - /** - * リアクションを編集 - */ + "noteUpdatedAt": string; "editReaction": string; - /** - * リアクションを削除 - */ "removeReaction": string; - /** - * ノートの削除を予約 - */ - "scheduledNoteDelete": string; - /** - * ニャ化を表示しない - */ "noNyaization": string; - /** - * ニャ化を含めて表示する - */ "revertNoNyaization": string; - /** - * テキストのソースを表示する - */ "viewTextSource": string; - /** - * ノート編集を続行しますか? - */ "disableNoteEditConfirm": string; - /** - * ノート編集に対応しているソフトウェア(Mastodon、CherryPick、FireFishなど)でのみ、編集された内容と履歴を見ることができます。 - * ノート編集に対応していないソフトウェアでは、ノートを編集する前の内容が表示されるので、すべての連合サーバーで修正した内容を反映させたい場合は、「削除して編集」でノートを書き直してください。 - */ "disableNoteEditConfirmWarn": string; - /** - * ノートを編集する - */ "disableNoteEditOk": string; - /** - * センシティブなメディアを開くとき - */ "nsfwOpenBehavior": string; - /** - * プロフィールを表示 - */ "previewNoteProfile": string; - /** - * ノートを編集しました。 - */ "noteEdited": string; - /** - * モーダル背景色を削除 - */ "removeModalBgColorForBlur": string; - /** - * このリリースをスキップする - */ "skipThisVersion": string; - /** - * プレリリース版の通知を受け取る - */ "enableReceivePrerelease": string; - /** - * 未発売バージョンのCherryPickを利用しています! - */ "youAreRunningBetaClient": string; - /** - * CherryPickアップデート - */ "cherrypickUpdate": string; - /** - * すべてのメディアノートを省略して表示 - */ "allMediaNoteCollapse": string; - /** - * 「インタラクト時に再生」に設定すると、画像の上にマウスを置いたり、画像をタッチすると再生されます。 - */ "showingAnimatedImagesDescription": string; - /** - * 返信に投稿フォームを表示する - */ "showFixedPostFormInReplies": string; - /** - * デスクトップとタブレット環境でのみ表示されます。 - */ "showFixedPostFormInRepliesDescription": string; - /** - * リノートと引用ボタンを分けて表示する - */ "renoteQuoteButtonSeparation": string; - /** - * 通知で返信があるノートの親ノートを表示する - */ "showReplyInNotification": string; - /** - * ノートに詳細表示ボタンを表示する - */ "infoButtonForNoteActions": string; - /** - * オプション「ノートの操作部をホバー時のみ表示する」をオンにしたときに適用されます。 - */ "infoButtonForNoteActionsDescription": string; - /** - * 「サーバーのマシン情報を公開する」設定がオフになっています。 - * サーバーメトリクスを表示するには、「コントロールパネル - その他」で「サーバーのマシン情報を公開する」設定を有効にしてください。 - */ "disabledServerMachineStats": string; - /** - * 初期設定のリプレイ - */ "replayUserSetupDialog": string; - /** - * チュートリアルのリプレイ - */ "replayTutorial": string; - /** - * にゃん! - */ "nya": string; - /** - * 一つだけ追加 - */ "addSingle": string; - /** - * 複数追加 - */ "addMultiple": string; - /** - * サブノートにアクションボタンを表示 - */ "showSubNoteFooterButton": string; - /** - * この設定を有効にすると、返信があるノートの親ノートにアクションボタンを表示します。 - */ "showSubNoteFooterButtonDescription": string; - /** - * フォローしました! - */ "alreadyFollowed": string; - /** - * ノート時刻を日付で表示 - */ "enableMarkByDate": string; - /** - * Renoteしますか? - */ "renoteConfirm": string; - /** - * この設定は「設定 - CherryPick」で変更できます。 - */ "renoteConfirmDescription": string; - /** - * 全ての招待コードを失効する - */ "inviteRevoke": string; - /** - * 本当に全ての招待コードを失効させますか? - */ "inviteRevokeConfirm": string; - /** - * 絶対時刻表記を使用する - */ "enableAbsoluteTime": string; - /** - * ノートを公開しました。 - */ "posted": string; - /** - * ノートを翻訳する - */ "translateNote": string; - /** - * ノート本文に翻訳ボタンを表示 - */ "showTranslateButtonInNote": string; - /** - * ニックネームを編集 - */ "editNickName": string; - /** - * ノートでアイコンを隠す - */ "hideAvatarsInNote": string; - /** - * バナー画像の表示 - */ "displayBanner": string; - /** - * ページの更新が必要なとき - */ "requireRefresh": string; - /** - * リソースを多く使用するため、デバイスの温度が高くなり、バッテリーの消耗が速くなる可能性があります - */ "performanceWarning": string; - /** - * 光敏感性発作を起こす可能性があります - */ "photosensitiveSeizuresWarning": string; - /** - * 通知領域を有効化 - */ "friendlyEnableNotifications": string; - /** - * 通知領域を無効化 - */ "friendlyDisableNotifications": string; - /** - * ウィジェット領域を有効化 - */ "friendlyEnableWidgets": string; - /** - * ウィジェット領域を無効化 - */ "friendlyDisableWidgets": string; - /** - * 文字を太くする - */ "useBoldFont": string; - /** - * 新しいノート通知を表示するとき - */ "newNoteReceivedNotification": string; - /** - * 右クリックを禁止 - */ "disableRightClick": string; - /** - * キャッシュをクリアしましょうか? - */ "cherrypickUpdatedCacheClearTitle": string; - /** - * テーマや色、ロケールなどの変更が正しく反映されない可能性があるため、クライアントが更新されたらキャッシュをクリアすることをお勧めします。 - * アカウントログイン状態はそのまま維持されます! - */ "cherrypickUpdatedCacheClear": string; - /** - * あとでキャッシュをクリアするには、設定 - キャッシュをクリアでできます! - */ "cherrypickUpdatedCacheClearLater": string; - /** - * ノートでつながるネットワーク - */ "headlineMisskey": string; - /** - * ようこそ!CherryPickは、オープンソースの分散型マイクロブログサービスです。 - * 「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡 - * 「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍 - * 新しい世界を探検しよう🚀 - */ "introMisskey": string; - /** - * {name}は、オープンソースのプラットフォームCherryPickのサーバーのひとつです。 - */ - "poweredByMisskeyDescription": ParameterizedString<"name">; - /** - * {month}月 {day}日 - */ - "monthAndDay": ParameterizedString<"month" | "day">; - /** - * 検索 - */ + "poweredByMisskeyDescription": string; + "monthAndDay": string; "search": string; - /** - * 通知 - */ "notifications": string; - /** - * ユーザー名 - */ "username": string; - /** - * パスワード - */ "password": string; - /** - * パスワードを忘れた - */ "forgotPassword": string; - /** - * 連合に照会中 - */ "fetchingAsApObject": string; - /** - * OK - */ "ok": string; - /** - * わかった - */ "gotIt": string; - /** - * キャンセル - */ "cancel": string; - /** - * やめておく - */ "noThankYou": string; - /** - * ユーザー名を入力 - */ "enterUsername": string; - /** - * {user}がリノート - */ - "renotedBy": ParameterizedString<"user">; - /** - * ノートはありません - */ + "renotedBy": string; "noNotes": string; - /** - * 通知はありません - */ "noNotifications": string; - /** - * サーバー - */ "instance": string; - /** - * 設定 - */ "settings": string; - /** - * 通知の設定 - */ "notificationSettings": string; - /** - * 基本設定 - */ "basicSettings": string; - /** - * その他の設定 - */ "otherSettings": string; - /** - * ウィンドウで開く - */ "openInWindow": string; - /** - * プロフィール - */ "profile": string; - /** - * タイムライン - */ "timeline": string; - /** - * 自己紹介はありません - */ "noAccountDescription": string; - /** - * ログイン - */ "login": string; - /** - * ログイン中 - */ "loggingIn": string; - /** - * ログアウト - */ "logout": string; - /** - * 全アカウントをログアウト - */ "logoutAll": string; - /** - * このデバイスに追加した全てのアカウントをログアウトしますか? - */ "logoutAllConfirm": string; - /** - * 新規登録 - */ "signup": string; - /** - * アップロード中 - */ "uploading": string; - /** - * 保存 - */ "save": string; - /** - * ユーザー - */ "users": string; - /** - * ユーザーを追加 - */ "addUser": string; - /** - * お気に入り - */ "favorite": string; - /** - * お気に入り - */ "favorites": string; - /** - * お気に入り解除 - */ "unfavorite": string; - /** - * お気に入りに登録しました。 - */ "favorited": string; - /** - * 既にお気に入りに登録されています。 - */ "alreadyFavorited": string; - /** - * お気に入りに登録できませんでした。 - */ "cantFavorite": string; - /** - * ピン留め - */ "pin": string; - /** - * ピン留め解除 - */ "unpin": string; - /** - * 内容をコピー - */ "copyContent": string; - /** - * リンクをコピー - */ "copyLink": string; - /** - * リノートのリンクをコピー - */ "copyLinkRenote": string; - /** - * 削除 - */ "delete": string; - /** - * 削除して編集 - */ "deleteAndEdit": string; - /** - * このノートを削除してもう一度編集しますか?このノートへのリアクション、リノート、返信も全て削除されます。 - */ "deleteAndEditConfirm": string; - /** - * 内容をコピーして編集 - */ "copyAndEdit": string; - /** - * このノートをコピーして編集しましょうか?ノートに含まれているメディアも一緒にコピーされます。 - */ "copyAndEditConfirm": string; - /** - * リストに追加 - */ "addToList": string; - /** - * アンテナに追加 - */ "addToAntenna": string; - /** - * メッセージを送信 - */ "sendMessage": string; - /** - * RSSをコピー - */ "copyRSS": string; - /** - * ユーザー名をコピー - */ "copyUsername": string; - /** - * ユーザーIDをコピー - */ "copyUserId": string; - /** - * ノートIDをコピー - */ "copyNoteId": string; - /** - * ファイルIDをコピー - */ "copyFileId": string; - /** - * フォルダーIDをコピー - */ "copyFolderId": string; - /** - * プロフィールURLをコピー - */ "copyProfileUrl": string; - /** - * ユーザーを検索 - */ "searchUser": string; - /** - * ユーザーのノートを検索 - */ - "searchThisUsersNotes": string; - /** - * 返信 - */ "reply": string; - /** - * もっと見る - */ "loadMore": string; - /** - * もっと見る - */ "showMore": string; - /** - * 閉じる - */ "showLess": string; - /** - * フォローされました - */ "youGotNewFollower": string; - /** - * フォローリクエストされました - */ "receiveFollowRequest": string; - /** - * フォローが承認されました - */ "followRequestAccepted": string; - /** - * メンション - */ "mention": string; - /** - * あなた宛て - */ "mentions": string; - /** - * ダイレクト投稿 - */ "directNotes": string; - /** - * インポートとエクスポート - */ "importAndExport": string; - /** - * インポート - */ "import": string; - /** - * エクスポート - */ "export": string; - /** - * ファイル - */ "files": string; - /** - * ダウンロード - */ "download": string; - /** - * ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。 - */ - "driveFileDeleteConfirm": ParameterizedString<"name">; - /** - * {name}のフォローを解除しますか? - */ - "unfollowConfirm": ParameterizedString<"name">; - /** - * エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。 - */ + "driveFileDeleteConfirm": string; + "unfollowConfirm": string; "exportRequested": string; - /** - * インポートをリクエストしました。これには時間がかかる場合があります。 - */ "importRequested": string; - /** - * リスト - */ "lists": string; - /** - * リストはありません - */ "noLists": string; - /** - * ノート - */ "note": string; - /** - * ノート - */ "notes": string; - /** - * フォロー - */ "following": string; - /** - * フォロワー - */ "followers": string; - /** - * フォローされています - */ "followsYou": string; - /** - * リスト作成 - */ "createList": string; - /** - * リストの管理 - */ "manageLists": string; - /** - * エラー - */ "error": string; - /** - * 問題が発生しました - */ "somethingHappened": string; - /** - * 再試行 - */ "retry": string; - /** - * ページの読み込みに失敗しました。 - */ "pageLoadError": string; - /** - * これは通常、ネットワークまたはブラウザキャッシュが原因です。キャッシュをクリアするか、しばらく待ってから再度試してください。 - */ "pageLoadErrorDescription": string; - /** - * サーバーの応答がありません。しばらく待ってから再度試してください。 - */ "serverIsDead": string; - /** - * このページを表示するためには、リロードして新しいバージョンのクライアントをご利用ください。 - */ "youShouldUpgradeClient": string; - /** - * リスト名を入力 - */ "enterListName": string; - /** - * プライバシー - */ "privacy": string; - /** - * フォローを承認制にする - */ "makeFollowManuallyApprove": string; - /** - * デフォルトの公開範囲 - */ "defaultNoteVisibility": string; - /** - * フォロー - */ "follow": string; - /** - * フォロー申請 - */ "followRequest": string; - /** - * フォロー申請 - */ "followRequests": string; - /** - * フォロー解除 - */ "unfollow": string; - /** - * フォロー許可待ち - */ "followRequestPending": string; - /** - * 絵文字を入力 - */ "enterEmoji": string; - /** - * リノート - */ "renote": string; - /** - * リノート解除 - */ "unrenote": string; - /** - * リノートしました。 - */ "renoted": string; - /** - * {name} にリノートしました。 - */ - "renotedToX": ParameterizedString<"name">; - /** - * ノートを引用しました。 - */ "quoted": string; - /** - * 返信を投稿しました。 - */ "replied": string; - /** - * この投稿はリノートできません。 - */ "cantRenote": string; - /** - * リノートをリノートすることはできません。 - */ "cantReRenote": string; - /** - * 引用 - */ "quote": string; - /** - * チャンネル内リノート - */ "inChannelRenote": string; - /** - * チャンネル内引用 - */ "inChannelQuote": string; - /** - * チャンネルにリノート - */ - "renoteToChannel": string; - /** - * 他のチャンネルにリノート - */ - "renoteToOtherChannel": string; - /** - * ピン留めされたノート - */ "pinnedNote": string; - /** - * ピン留め - */ "pinned": string; - /** - * あなた - */ "you": string; - /** - * クリックして表示 - */ "clickToShow": string; - /** - * ダブルクリックして表示 - */ "doubleClickToShow": string; - /** - * センシティブ - */ "sensitive": string; - /** - * 追加 - */ "add": string; - /** - * リアクション - */ "reaction": string; - /** - * リアクション - */ "reactions": string; - /** - * 絵文字ピッカー - */ "emojiPicker": string; - /** - * リアクション時にピン留め表示する絵文字を設定できます - */ "pinnedEmojisForReactionSettingDescription": string; - /** - * 絵文字入力時にピン留め表示する絵文字を設定できます - */ "pinnedEmojisSettingDescription": string; - /** - * ピッカーの表示 - */ "emojiPickerDisplay": string; - /** - * リアクション設定から上書きする - */ "overwriteFromPinnedEmojisForReaction": string; - /** - * 全般設定から上書きする - */ "overwriteFromPinnedEmojis": string; - /** - * ドラッグして並び替え、クリックして削除、+を押して追加します。 - */ "reactionSettingDescription2": string; - /** - * 公開範囲を記憶する - */ "rememberNoteVisibility": string; - /** - * 添付取り消し - */ "attachCancel": string; - /** - * ファイルを削除 - */ - "deleteFile": string; - /** - * センシティブとして設定 - */ "markAsSensitive": string; - /** - * センシティブを解除する - */ "unmarkAsSensitive": string; - /** - * ファイル名を入力 - */ "enterFileName": string; - /** - * ミュート - */ "mute": string; - /** - * ミュート解除 - */ "unmute": string; - /** - * リノートをミュート - */ "renoteMute": string; - /** - * リノートのミュートを解除 - */ "renoteUnmute": string; - /** - * ブロック - */ "block": string; - /** - * ブロック解除 - */ "unblock": string; - /** - * 凍結 - */ "suspend": string; - /** - * 解凍 - */ "unsuspend": string; - /** - * センシティブとして設定 - */ - "setAsSensitive": string; - /** - * センシティブを解除 - */ - "unsetAsSensitive": string; - /** - * ブロックしますか? - */ "blockConfirm": string; - /** - * ブロック解除しますか? - */ "unblockConfirm": string; - /** - * 凍結しますか? - */ "suspendConfirm": string; - /** - * 解凍しますか? - */ "unsuspendConfirm": string; - /** - * センシティブとして設定しますか? - */ - "setSensitiveConfirm": string; - /** - * センシティブを解除しますか? - */ - "unsetSensitiveConfirm": string; - /** - * リストを選択 - */ "selectList": string; - /** - * リストを編集 - */ "editList": string; - /** - * チャンネルを選択 - */ "selectChannel": string; - /** - * アンテナを選択 - */ "selectAntenna": string; - /** - * アンテナを編集 - */ "editAntenna": string; - /** - * アンテナを作成 - */ - "createAntenna": string; - /** - * ウィジェットを選択 - */ "selectWidget": string; - /** - * ウィジェットを編集 - */ "editWidgets": string; - /** - * 編集を終了 - */ "editWidgetsExit": string; - /** - * カスタム絵文字 - */ "customEmojis": string; - /** - * 絵文字 - */ "emoji": string; - /** - * 絵文字 - */ "emojis": string; - /** - * 絵文字名 - */ "emojiName": string; - /** - * 絵文字画像URL - */ "emojiUrl": string; - /** - * 絵文字を追加 - */ "addEmoji": string; - /** - * おすすめ設定 - */ "settingGuide": string; - /** - * リモートのファイルをキャッシュする - */ "cacheRemoteFiles": string; - /** - * この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持しますが、画像のサムネイル生成やユーザーのプライバシー保護のために、default.ymlでproxyRemoteFilesをtrueにすることをお勧めします。 - */ "cacheRemoteFilesDescription": string; - /** - * ファイル管理の🗑️ボタンで全てのキャッシュを削除できます。 - */ "youCanCleanRemoteFilesCache": string; - /** - * リモートのセンシティブなファイルをキャッシュする - */ "cacheRemoteSensitiveFiles": string; - /** - * この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。 - */ "cacheRemoteSensitiveFilesDescription": string; - /** - * Botとして設定 - */ "flagAsBot": string; - /** - * このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、CherryPickのシステム上での扱いがBotに合ったものになります。 - */ "flagAsBotDescription": string; - /** - * にゃああああああああああああああ!!!!!!!!!!!! - */ "flagAsCat": string; - /** - * にゃにゃにゃ?? - */ "flagAsCatDescription": string; - /** - * センシティブとして設定 - */ - "flagAsSensitive": string; - /** - * このオプションを有効にすると、このアカウントはセンシティブなアカウントとして他者に表示されます。この機能を有効にすると、検索結果に表示されなくなります。現在ローカル限定の機能です。 - */ - "flagAsSensitiveDescription": string; - /** - * タイムラインにノートへの返信を表示する - */ "flagShowTimelineReplies": string; - /** - * オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。 - */ "flagShowTimelineRepliesDescription": string; - /** - * フォロー中ユーザーからのフォロリクを自動承認 - */ "autoAcceptFollowed": string; - /** - * Botからのフォローを承認制にする - */ - "carefulBot": string; - /** - * この設定を有効にすると、Botからのフォローリクエストを承認制にします。 - */ - "carefulBotDescription": string; - /** - * アカウントを追加 - */ "addAccount": string; - /** - * アカウントリストの情報を更新 - */ "reloadAccountsList": string; - /** - * ログインに失敗しました - */ "loginFailed": string; - /** - * リモートで表示 - */ "showOnRemote": string; - /** - * リモートで続行 - */ - "continueOnRemote": string; - /** - * Misskey Hubからサーバーを選択 - */ - "chooseServerOnMisskeyHub": string; - /** - * サーバーのドメインを直接指定 - */ - "specifyServerHost": string; - /** - * ドメインを入力してください - */ - "inputHostName": string; - /** - * 全般 - */ "general": string; - /** - * 壁紙 - */ "wallpaper": string; - /** - * 壁紙を設定 - */ "setWallpaper": string; - /** - * 壁紙を削除 - */ "removeWallpaper": string; - /** - * 検索: {q} - */ - "searchWith": ParameterizedString<"q">; - /** - * リストがありません - */ + "searchWith": string; "youHaveNoLists": string; - /** - * {name}をフォローしますか? - */ - "followConfirm": ParameterizedString<"name">; - /** - * プロキシアカウント - */ + "followConfirm": string; "proxyAccount": string; - /** - * プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。 - */ "proxyAccountDescription": string; - /** - * ホスト - */ "host": string; - /** - * 自分を選択 - */ - "selectSelf": string; - /** - * ユーザーを選択 - */ "selectUser": string; - /** - * 宛先 - */ "recipient": string; - /** - * 注釈 - */ "annotation": string; - /** - * 連合 - */ "federation": string; - /** - * サーバー - */ "instances": string; - /** - * 初観測 - */ "registeredAt": string; - /** - * 直近のリクエスト受信 - */ "latestRequestReceivedAt": string; - /** - * 直近のステータス - */ "latestStatus": string; - /** - * ストレージ使用量 - */ "storageUsage": string; - /** - * チャート - */ "charts": string; - /** - * 1時間ごと - */ "perHour": string; - /** - * 1日ごと - */ "perDay": string; - /** - * アクティビティの配送を停止 - */ "stopActivityDelivery": string; - /** - * このサーバーをブロック - */ "blockThisInstance": string; - /** - * サーバーをサイレンス - */ "silenceThisInstance": string; - /** - * サーバーをメディアサイレンス - */ - "mediaSilenceThisInstance": string; - /** - * 操作 - */ "operations": string; - /** - * ソフトウェア - */ "software": string; - /** - * バージョン - */ "version": string; - /** - * メタデータ - */ "metadata": string; - /** - * {n}つのファイル - */ - "withNFiles": ParameterizedString<"n">; - /** - * モニター - */ + "withNFiles": string; "monitor": string; - /** - * ジョブキュー - */ "jobQueue": string; - /** - * CPUとメモリ - */ "cpuAndMemory": string; - /** - * ネットワーク - */ "network": string; - /** - * ディスク - */ "disk": string; - /** - * サーバー情報 - */ "instanceInfo": string; - /** - * 統計 - */ "statistics": string; - /** - * キューをクリア - */ "clearQueue": string; - /** - * キューをクリアしますか? - */ "clearQueueConfirmTitle": string; - /** - * 未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。 - */ "clearQueueConfirmText": string; - /** - * キャッシュをクリア - */ "clearCachedFiles": string; - /** - * キャッシュされたリモートファイルをすべて削除しますか? - */ "clearCachedFilesConfirm": string; - /** - * ブロックしたサーバー - */ "blockedInstances": string; - /** - * ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。 - */ "blockedInstancesDescription": string; - /** - * サイレンスしたサーバー - */ "silencedInstances": string; - /** - * サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。 - */ "silencedInstancesDescription": string; - /** - * メディアサイレンスしたサーバー - */ - "mediaSilencedInstances": string; - /** - * メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。 - */ - "mediaSilencedInstancesDescription": string; - /** - * ミュートとブロック - */ "muteAndBlock": string; - /** - * ミュートしたユーザー - */ "mutedUsers": string; - /** - * ブロックしたユーザー - */ "blockedUsers": string; - /** - * ユーザーはいません - */ "noUsers": string; - /** - * プロフィールを編集 - */ "editProfile": string; - /** - * このノートを削除しますか? - */ "noteDeleteConfirm": string; - /** - * これ以上ピン留めできません - */ "pinLimitExceeded": string; - /** - * CherryPickのインストールが完了しました!管理者アカウントを作成しましょう。 - */ "intro": string; - /** - * 完了 - */ "done": string; - /** - * 処理中 - */ "processing": string; - /** - * プレビュー - */ "preview": string; - /** - * デフォルト - */ "default": string; - /** - * デフォルト: {value} - */ - "defaultValueIs": ParameterizedString<"value">; - /** - * 絵文字はありません - */ + "defaultValueIs": string; "noCustomEmojis": string; - /** - * ジョブはありません - */ "noJobs": string; - /** - * 連合中 - */ "federating": string; - /** - * ブロック中 - */ "blocked": string; - /** - * 配信停止 - */ "suspended": string; - /** - * 全て - */ "all": string; - /** - * 購読中 - */ "subscribing": string; - /** - * 配信中 - */ "publishing": string; - /** - * 応答なし - */ "notResponding": string; - /** - * サーバーのフォロー - */ "instanceFollowing": string; - /** - * サーバーのフォロワー - */ "instanceFollowers": string; - /** - * サーバーのユーザー - */ "instanceUsers": string; - /** - * パスワードを変更 - */ "changePassword": string; - /** - * セキュリティ - */ "security": string; - /** - * 入力が一致しません。 - */ "retypedNotMatch": string; - /** - * 現在のパスワード - */ "currentPassword": string; - /** - * 新しいパスワード - */ "newPassword": string; - /** - * 新しいパスワード(再入力) - */ "newPasswordRetype": string; - /** - * ファイルを添付 - */ "attachFile": string; - /** - * もっと! - */ "more": string; - /** - * ハイライト - */ "featured": string; - /** - * ユーザー名かユーザーID - */ "usernameOrUserId": string; - /** - * ユーザーが見つかりません - */ "noSuchUser": string; - /** - * 照会 - */ "lookup": string; - /** - * お知らせ - */ "announcements": string; - /** - * 画像URL - */ "imageUrl": string; - /** - * 削除 - */ "remove": string; - /** - * 削除しました - */ "removed": string; - /** - * 「{x}」を削除しますか? - */ - "removeAreYouSure": ParameterizedString<"x">; - /** - * 「{x}」を削除しますか? - */ - "deleteAreYouSure": ParameterizedString<"x">; - /** - * リセットしますか? - */ + "removeAreYouSure": string; + "deleteAreYouSure": string; "resetAreYouSure": string; - /** - * よろしいですか? - */ "areYouSure": string; - /** - * 保存しました - */ "saved": string; - /** - * チャット - */ "messaging": string; - /** - * アップロード - */ "upload": string; - /** - * オリジナル画像を保持 - */ "keepOriginalUploading": string; - /** - * 画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。 - */ "keepOriginalUploadingDescription": string; - /** - * ドライブから - */ "fromDrive": string; - /** - * URLから - */ "fromUrl": string; - /** - * URLアップロード - */ "uploadFromUrl": string; - /** - * アップロードしたいファイルのURL - */ "uploadFromUrlDescription": string; - /** - * アップロードをリクエストしました - */ "uploadFromUrlRequested": string; - /** - * アップロードが完了するまで時間がかかる場合があります。 - */ "uploadFromUrlMayTakeTime": string; - /** - * みつける - */ "explore": string; - /** - * 既読 - */ "messageRead": string; - /** - * 送信済み - */ "messageSend": string; - /** - * これより過去の履歴はありません - */ "noMoreHistory": string; - /** - * チャットを開始 - */ "startMessaging": string; - /** - * {n}人が読みました - */ - "nUsersRead": ParameterizedString<"n">; - /** - * {0}に同意 - */ - "agreeTo": ParameterizedString<"0">; - /** - * 同意する - */ + "nUsersRead": string; + "agreeTo": string; "agree": string; - /** - * 下記に同意する - */ "agreeBelow": string; - /** - * 基本的な注意事項 - */ "basicNotesBeforeCreateAccount": string; - /** - * 利用規約 - */ "termsOfService": string; - /** - * 始める - */ "start": string; - /** - * ホーム - */ "home": string; - /** - * リモートユーザーのため、情報が不完全です。 - */ "remoteUserCaution": string; - /** - * センシティブなユーザーです。 - */ - "sensitiveUserCaution": string; - /** - * この機能は非推奨です。 - * 必要であれば、バックアップを取ることをお勧めします。 - */ - "deprecatedCaution": string; - /** - * アクティビティ - */ "activity": string; - /** - * 画像 - */ "images": string; - /** - * 画像 - */ "image": string; - /** - * 誕生日 - */ "birthday": string; - /** - * {age}歳 - */ - "yearsOld": ParameterizedString<"age">; - /** - * 登録日 - */ + "yearsOld": string; "registeredDate": string; - /** - * 場所 - */ "location": string; - /** - * テーマ - */ "theme": string; - /** - * ライトモードで使うテーマ - */ "themeForLightMode": string; - /** - * ダークモードで使うテーマ - */ "themeForDarkMode": string; - /** - * ライト - */ "light": string; - /** - * ダーク - */ "dark": string; - /** - * 明るいテーマ - */ "lightThemes": string; - /** - * 暗いテーマ - */ "darkThemes": string; - /** - * デバイスのダークモードと同期する - */ "syncDeviceDarkMode": string; - /** - * ドライブ - */ "drive": string; - /** - * ファイル名 - */ "fileName": string; - /** - * ファイルを選択 - */ "selectFile": string; - /** - * ファイルを選択 - */ "selectFiles": string; - /** - * フォルダーを選択 - */ "selectFolder": string; - /** - * フォルダーを選択 - */ "selectFolders": string; - /** - * ファイルが選択されていません - */ - "fileNotSelected": string; - /** - * ファイル名を変更 - */ "renameFile": string; - /** - * フォルダー名 - */ "folderName": string; - /** - * フォルダーを作成 - */ "createFolder": string; - /** - * フォルダー名を変更 - */ "renameFolder": string; - /** - * フォルダーを削除 - */ "deleteFolder": string; - /** - * フォルダー - */ "folder": string; - /** - * ファイルを追加 - */ "addFile": string; - /** - * ドライブは空です - */ "emptyDrive": string; - /** - * フォルダーは空です - */ "emptyFolder": string; - /** - * 削除できません - */ "unableToDelete": string; - /** - * 新しいファイル名を入力してください - */ "inputNewFileName": string; - /** - * 新しいキャプションを入力してください - */ "inputNewDescription": string; - /** - * 新しいフォルダ名を入力してください - */ "inputNewFolderName": string; - /** - * 移動先のフォルダーは、移動するフォルダーのサブフォルダーです。 - */ "circularReferenceFolder": string; - /** - * このフォルダは空でないため、削除できません。 - */ "hasChildFilesOrFolders": string; - /** - * URLをコピー - */ "copyUrl": string; - /** - * 名前を変更 - */ "rename": string; - /** - * アイコン - */ "avatar": string; - /** - * バナー - */ "banner": string; - /** - * センシティブなメディアの表示 - */ "displayOfSensitiveMedia": string; - /** - * サーバーとの接続が失われたとき - */ "whenServerDisconnected": string; - /** - * サーバーから切断されました - */ "disconnectedFromServer": string; - /** - * リロード - */ "reload": string; - /** - * なにもしない - */ "doNothing": string; - /** - * リロードしますか? - */ "reloadConfirm": string; - /** - * ウォッチ - */ "watch": string; - /** - * ウォッチ解除 - */ "unwatch": string; - /** - * 許可 - */ "accept": string; - /** - * 拒否 - */ "reject": string; - /** - * 通常 - */ "normal": string; - /** - * サーバー名 - */ "instanceName": string; - /** - * サーバーの紹介 - */ "instanceDescription": string; - /** - * 管理者の名前 - */ "maintainerName": string; - /** - * 管理者のメールアドレス - */ "maintainerEmail": string; - /** - * 利用規約URL - */ "tosUrl": string; - /** - * 今年 - */ "thisYear": string; - /** - * 今月 - */ "thisMonth": string; - /** - * 今日 - */ "today": string; - /** - * {day}日 - */ - "dayX": ParameterizedString<"day">; - /** - * {month}月 - */ - "monthX": ParameterizedString<"month">; - /** - * {year}年 - */ - "yearX": ParameterizedString<"year">; - /** - * ページ - */ + "dayX": string; + "monthX": string; + "yearX": string; "pages": string; - /** - * 連携 - */ "integration": string; - /** - * 接続する - */ "connectService": string; - /** - * 切断する - */ "disconnectService": string; - /** - * ローカルタイムラインを有効にする - */ "enableLocalTimeline": string; - /** - * グローバルタイムラインを有効にする - */ "enableGlobalTimeline": string; - /** - * これらのタイムラインを無効化しても、利便性のため管理者およびモデレーターは引き続き利用することができます。 - */ "disablingTimelinesInfo": string; - /** - * 登録 - */ "registration": string; - /** - * 誰でも新規登録できるようにする - */ "enableRegistration": string; - /** - * 招待 - */ "invite": string; - /** - * ローカルユーザーひとりあたりのドライブ容量 - */ "driveCapacityPerLocalAccount": string; - /** - * リモートユーザーひとりあたりのドライブ容量 - */ "driveCapacityPerRemoteAccount": string; - /** - * メガバイト単位 - */ "inMb": string; - /** - * バナー画像のURL - */ "bannerUrl": string; - /** - * 背景画像のURL - */ "backgroundImageUrl": string; - /** - * 基本情報 - */ "basicInfo": string; - /** - * ピン留めユーザー - */ "pinnedUsers": string; - /** - * 「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。 - */ "pinnedUsersDescription": string; - /** - * ピン留めページ - */ "pinnedPages": string; - /** - * サーバーのトップページにピン留めしたいページのパスを改行で区切って記述します。 - */ "pinnedPagesDescription": string; - /** - * ピン留めするクリップのID - */ "pinnedClipId": string; - /** - * ピン留めされたノート - */ "pinnedNotes": string; - /** - * hCaptcha - */ "hcaptcha": string; - /** - * hCaptchaを有効にする - */ "enableHcaptcha": string; - /** - * サイトキー - */ "hcaptchaSiteKey": string; - /** - * シークレットキー - */ "hcaptchaSecretKey": string; - /** - * mCaptcha - */ - "mcaptcha": string; - /** - * mCaptchaを有効にする - */ - "enableMcaptcha": string; - /** - * サイトキー - */ - "mcaptchaSiteKey": string; - /** - * シークレットキー - */ - "mcaptchaSecretKey": string; - /** - * mCaptchaのインスタンスのURL - */ - "mcaptchaInstanceUrl": string; - /** - * reCAPTCHA - */ "recaptcha": string; - /** - * reCAPTCHAを有効にする - */ "enableRecaptcha": string; - /** - * サイトキー - */ "recaptchaSiteKey": string; - /** - * シークレットキー - */ "recaptchaSecretKey": string; - /** - * Turnstile - */ "turnstile": string; - /** - * Turnstileを有効にする - */ "enableTurnstile": string; - /** - * サイトキー - */ "turnstileSiteKey": string; - /** - * シークレットキー - */ "turnstileSecretKey": string; - /** - * 複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。 - */ "avoidMultiCaptchaConfirm": string; - /** - * アンテナ - */ "antennas": string; - /** - * アンテナの管理 - */ "manageAntennas": string; - /** - * 名前 - */ "name": string; - /** - * 受信ソース - */ "antennaSource": string; - /** - * 受信キーワード - */ "antennaKeywords": string; - /** - * 除外キーワード - */ "antennaExcludeKeywords": string; - /** - * Botアカウントを除外 - */ - "antennaExcludeBots": string; - /** - * スペースで区切るとAND指定になり、改行で区切るとOR指定になります - */ "antennaKeywordsDescription": string; - /** - * 新しいノートを通知する - */ "notifyAntenna": string; - /** - * ファイルが添付されたノートのみ - */ "withFileAntenna": string; - /** - * ブラウザへのプッシュ通知を有効にする - */ "enableServiceworker": string; - /** - * ユーザー名を改行で区切って指定します - */ "antennaUsersDescription": string; - /** - * 大文字小文字を区別する - */ "caseSensitive": string; - /** - * 返信を含む - */ "withReplies": string; - /** - * 次のアカウントに接続されています - */ "connectedTo": string; - /** - * 投稿と返信 - */ "notesAndReplies": string; - /** - * ファイル付き - */ "withFiles": string; - /** - * サイレンス - */ "silence": string; - /** - * サイレンスしますか? - */ "silenceConfirm": string; - /** - * サイレンス解除 - */ "unsilence": string; - /** - * サイレンス解除しますか? - */ "unsilenceConfirm": string; - /** - * 人気のユーザー - */ "popularUsers": string; - /** - * 最近投稿したユーザー - */ "recentlyUpdatedUsers": string; - /** - * 最近登録したユーザー - */ "recentlyRegisteredUsers": string; - /** - * 最近発見されたユーザー - */ "recentlyDiscoveredUsers": string; - /** - * {count}のユーザーがいます - */ - "exploreUsersCount": ParameterizedString<"count">; - /** - * Fediverseを探索 - */ + "exploreUsersCount": string; "exploreFediverse": string; - /** - * 人気のタグ - */ "popularTags": string; - /** - * リスト - */ "userList": string; - /** - * 情報 - */ "about": string; - /** - * CherryPickについて - */ "aboutMisskey": string; - /** - * 管理者 - */ "administrator": string; - /** - * 確認コード - */ "token": string; - /** - * 二要素認証 - */ "2fa": string; - /** - * 二要素認証のセットアップ - */ "setupOf2fa": string; - /** - * 認証アプリ - */ "totp": string; - /** - * 認証アプリを使ってワンタイムパスワードを入力 - */ "totpDescription": string; - /** - * モデレーター - */ "moderator": string; - /** - * モデレーション - */ "moderation": string; - /** - * モデレーションノート - */ "moderationNote": string; - /** - * モデレーションノートを追加する - */ "addModerationNote": string; - /** - * モデログ - */ "moderationLogs": string; - /** - * {n}人が投稿 - */ - "nUsersMentioned": ParameterizedString<"n">; - /** - * セキュリティキー・パスキー - */ + "nUsersMentioned": string; "securityKeyAndPasskey": string; - /** - * セキュリティキー - */ "securityKey": string; - /** - * 最後の使用 - */ "lastUsed": string; - /** - * 最後の使用: {t} - */ - "lastUsedAt": ParameterizedString<"t">; - /** - * 登録を解除 - */ + "lastUsedAt": string; "unregister": string; - /** - * パスワードレスログイン - */ "passwordLessLogin": string; - /** - * パスワードを使用せず、セキュリティキーやパスキーなどのみでログインします - */ "passwordLessLoginDescription": string; - /** - * パスワードをリセット - */ "resetPassword": string; - /** - * 新しいパスワードは「{password}」です - */ - "newPasswordIs": ParameterizedString<"password">; - /** - * UIのアニメーションを減らす - */ + "newPasswordIs": string; "reduceUiAnimation": string; - /** - * 共有 - */ "share": string; - /** - * 見つかりません - */ "notFound": string; - /** - * 指定されたURLに該当するページはありませんでした。 - */ "notFoundDescription": string; - /** - * 既定アップロード先 - */ "uploadFolder": string; - /** - * すべての通知を既読にする - */ "markAsReadAllNotifications": string; - /** - * すべての投稿を既読にする - */ "markAsReadAllUnreadNotes": string; - /** - * すべてのチャットを既読にする - */ "markAsReadAllTalkMessages": string; - /** - * ヘルプ - */ "help": string; - /** - * ここにメッセージを入力 - */ "inputMessageHere": string; - /** - * 閉じる - */ "close": string; - /** - * グループ - */ "group": string; - /** - * グループ - */ "groups": string; - /** - * グループを作成 - */ "createGroup": string; - /** - * 所有グループ - */ "ownedGroups": string; - /** - * 参加しているグループ - */ "joinedGroups": string; - /** - * 招待 - */ "invites": string; - /** - * グループ名 - */ "groupName": string; - /** - * メンバー - */ "members": string; - /** - * 譲渡 - */ "transfer": string; - /** - * ユーザーとチャット - */ "messagingWithUser": string; - /** - * グループでチャット - */ "messagingWithGroup": string; - /** - * タイトル - */ "title": string; - /** - * テキスト - */ "text": string; - /** - * 有効にする - */ "enable": string; - /** - * 次 - */ "next": string; - /** - * 再入力 - */ "retype": string; - /** - * {user}のノート - */ - "noteOf": ParameterizedString<"user">; - /** - * グループに招待 - */ + "noteOf": string; "inviteToGroup": string; - /** - * 引用付き - */ "quoteAttached": string; - /** - * 引用として添付しますか? - */ "quoteQuestion": string; - /** - * クリップボードのテキストが長いです。テキストファイルとして添付しますか? - */ - "attachAsFileQuestion": string; - /** - * まだチャットはありません - */ "noMessagesYet": string; - /** - * 新しいメッセージがあります - */ "newMessageExists": string; - /** - * メッセージに添付できるファイルはひとつです - */ "onlyOneFileCanBeAttached": string; - /** - * 続行する前に、登録またはログインが必要です - */ "signinRequired": string; - /** - * 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります - */ - "signinOrContinueOnRemote": string; - /** - * 招待 - */ "invitations": string; - /** - * 招待コード - */ "invitationCode": string; - /** - * 確認しています - */ "checking": string; - /** - * 利用できます - */ "available": string; - /** - * 利用できません - */ "unavailable": string; - /** - * a~z、A~Z、0~9、_が使えます - */ "usernameInvalidFormat": string; - /** - * 短すぎます - */ "tooShort": string; - /** - * 長すぎます - */ "tooLong": string; - /** - * 弱いパスワード - */ "weakPassword": string; - /** - * 普通のパスワード - */ "normalPassword": string; - /** - * 強いパスワード - */ "strongPassword": string; - /** - * 一致しました - */ "passwordMatched": string; - /** - * 一致していません - */ "passwordNotMatched": string; - /** - * {x}でログイン - */ - "signinWith": ParameterizedString<"x">; - /** - * ログインできませんでした。ユーザー名とパスワードを確認してください。 - */ + "signinWith": string; "signinFailed": string; - /** - * もしくは - */ "or": string; - /** - * 言語 - */ "language": string; - /** - * UIの表示言語 - */ "uiLanguage": string; - /** - * グループに招待されました - */ "groupInvited": string; - /** - * {x}について - */ - "aboutX": ParameterizedString<"x">; - /** - * 絵文字のスタイル - */ + "aboutX": string; "emojiStyle": string; - /** - * ネイティブ - */ "native": string; - /** - * メニューをドロワーで表示しない - */ "disableDrawer": string; - /** - * グループがありません - */ "youHaveNoGroups": string; - /** - * 既存のグループに招待してもらうか、新しくグループを作成してください。 - */ "joinOrCreateGroup": string; - /** - * ノートのアクションをホバー時のみ表示する - */ "showNoteActionsOnlyHover": string; - /** - * ノートのリアクション数を表示する - */ - "showReactionsCount": string; - /** - * 履歴はありません - */ "noHistory": string; - /** - * ログイン履歴 - */ "signinHistory": string; - /** - * 高度なMFMを有効にする - */ "enableAdvancedMfm": string; - /** - * 有効にすると、動きのあるMFMのようなさまざまなMFM機能が使用できます。 - */ "enableAdvancedMfmDescription": string; - /** - * 動きのあるMFMを有効にする - */ "enableAnimatedMfm": string; - /** - * 有効にすると、MFM文法または絵文字を使用するテキストが動きます。 - */ "enableAnimatedMfmDescription": string; - /** - * やっています - */ "doing": string; - /** - * カテゴリ - */ "category": string; - /** - * タグ - */ "tags": string; - /** - * このドキュメントのソース - */ "docSource": string; - /** - * アカウントを作成 - */ "createAccount": string; - /** - * 既存のアカウント - */ "existingAccount": string; - /** - * 再生成 - */ "regenerate": string; - /** - * フォントサイズ - */ "fontSize": string; - /** - * 画像が1枚のみのメディアリストの高さ - */ "mediaListWithOneImageAppearance": string; - /** - * {x}を上限に - */ - "limitTo": ParameterizedString<"x">; - /** - * フォロー申請はありません - */ + "limitTo": string; "noFollowRequests": string; - /** - * 画像を新しいタブで開く - */ "openImageInNewTab": string; - /** - * ダッシュボード - */ "dashboard": string; - /** - * ローカル - */ "local": string; - /** - * リモート - */ "remote": string; - /** - * 合計 - */ "total": string; - /** - * 前週比 - */ "weekOverWeekChanges": string; - /** - * 前日比 - */ "dayOverDayChanges": string; - /** - * アピアランス - */ "appearance": string; - /** - * クライアント設定 - */ "clientSettings": string; - /** - * アカウント設定 - */ "accountSettings": string; - /** - * プロモーション - */ "promotion": string; - /** - * プロモート - */ "promote": string; - /** - * 日数 - */ "numberOfDays": string; - /** - * このノートを非表示 - */ "hideThisNote": string; - /** - * タイムラインにおすすめのノートを表示する - */ "showFeaturedNotesInTimeline": string; - /** - * オブジェクトストレージ - */ "objectStorage": string; - /** - * オブジェクトストレージを使用 - */ "useObjectStorage": string; - /** - * Base URL - */ "objectStorageBaseUrl": string; - /** - * 参照に使用するURL。CDNやProxyを使用している場合はそのURL、S3: 'https://.s3.amazonaws.com'、GCS等: 'https://storage.googleapis.com/'。 - */ "objectStorageBaseUrlDesc": string; - /** - * Bucket - */ "objectStorageBucket": string; - /** - * 使用サービスのbucket名を指定してください。 - */ "objectStorageBucketDesc": string; - /** - * Prefix - */ "objectStoragePrefix": string; - /** - * このprefixのディレクトリ下に格納されます。 - */ "objectStoragePrefixDesc": string; - /** - * Endpoint - */ "objectStorageEndpoint": string; - /** - * S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。''または':'のように指定します。 - */ "objectStorageEndpointDesc": string; - /** - * Region - */ "objectStorageRegion": string; - /** - * 'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。 - */ "objectStorageRegionDesc": string; - /** - * SSLを使用する - */ "objectStorageUseSSL": string; - /** - * API接続にhttpsを使用しない場合はオフにしてください - */ "objectStorageUseSSLDesc": string; - /** - * Proxyを利用する - */ "objectStorageUseProxy": string; - /** - * API接続にproxyを利用しない場合はオフにしてください - */ "objectStorageUseProxyDesc": string; - /** - * アップロード時に'public-read'を設定する - */ "objectStorageSetPublicRead": string; - /** - * s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。 - */ "s3ForcePathStyleDesc": string; - /** - * サーバーログ - */ "serverLogs": string; - /** - * 全て削除 - */ "deleteAll": string; - /** - * タイムライン上部に投稿フォームを表示する - */ "showFixedPostForm": string; - /** - * タイムライン上部に投稿フォームを表示する(チャンネル) - */ "showFixedPostFormInChannel": string; - /** - * フォローする際、デフォルトで返信をTLに含むようにする - */ "withRepliesByDefaultForNewlyFollowed": string; - /** - * 新しいノートがあります - */ "newNoteRecived": string; - /** - * {n}個の新しいノートがあります - */ - "newNoteRecivedCount": ParameterizedString<"n">; - /** - * サウンド - */ + "newNoteRecivedCount": string; "sounds": string; - /** - * サウンド - */ "sound": string; - /** - * 振動 - */ "vibrations": string; - /** - * サウンドと振動 - */ "soundsAndVibrations": string; - /** - * 振動再生 - */ "playVibrations": string; - /** - * この機能はiOS/iPadOSではサポートされていません。 - */ "playVibrationsDescription": string; - /** - * 聴く - */ "listen": string; - /** - * なし - */ "none": string; - /** - * ページで表示 - */ "showInPage": string; - /** - * ポップアウト - */ "popout": string; - /** - * 音量 - */ "volume": string; - /** - * マスター音量 - */ "masterVolume": string; - /** - * サウンドを出力しない - */ "notUseSound": string; - /** - * CherryPickがアクティブな時のみサウンドを出力する - */ "useSoundOnlyWhenActive": string; - /** - * 詳細 - */ "details": string; - /** - * 絵文字を選択 - */ "chooseEmoji": string; - /** - * 操作を完了できません - */ "unableToProcess": string; - /** - * 最近使用 - */ "recentUsed": string; - /** - * インストール - */ "install": string; - /** - * アンインストール - */ "uninstall": string; - /** - * インストールされたアプリ - */ "installedApps": string; - /** - * ありません - */ "nothing": string; - /** - * インストール日時 - */ "installedDate": string; - /** - * 最終使用日時 - */ "lastUsedDate": string; - /** - * 状態 - */ "state": string; - /** - * ソート - */ "sort": string; - /** - * 昇順 - */ "ascendingOrder": string; - /** - * 降順 - */ "descendingOrder": string; - /** - * スクラッチパッド - */ "scratchpad": string; - /** - * スクラッチパッドは、AiScriptの実験環境を提供します。CherryPickと対話するコードの記述、実行、結果の確認ができます。 - */ "scratchpadDescription": string; - /** - * 出力 - */ "output": string; - /** - * スクリプト - */ "script": string; - /** - * Pagesのスクリプトを無効にする - */ "disablePagesScript": string; - /** - * リモートユーザー情報の更新 - */ "updateRemoteUser": string; - /** - * アイコンを解除 - */ "unsetUserAvatar": string; - /** - * アイコンを解除しますか? - */ "unsetUserAvatarConfirm": string; - /** - * バナーを解除 - */ "unsetUserBanner": string; - /** - * バナーを解除しますか? - */ "unsetUserBannerConfirm": string; - /** - * すべてのファイルを削除 - */ "deleteAllFiles": string; - /** - * すべてのファイルを削除しますか? - */ "deleteAllFilesConfirm": string; - /** - * フォローを全解除 - */ "removeAllFollowing": string; - /** - * {host}からのフォローをすべて解除します。そのサーバーがもう存在しなくなった場合などに実行してください。 - */ - "removeAllFollowingDescription": ParameterizedString<"host">; - /** - * このユーザーは凍結されています。 - */ + "removeAllFollowingDescription": string; "userSuspended": string; - /** - * このユーザーはサイレンスされています。 - */ "userSilenced": string; - /** - * アカウントが凍結されています - */ "yourAccountSuspendedTitle": string; - /** - * このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。 - */ "yourAccountSuspendedDescription": string; - /** - * トークンが無効です - */ "tokenRevoked": string; - /** - * ログイントークンが失効しています。ログインし直してください。 - */ "tokenRevokedDescription": string; - /** - * アカウントは削除されています - */ "accountDeleted": string; - /** - * このアカウントは削除されています。 - */ "accountDeletedDescription": string; - /** - * メニュー - */ "menu": string; - /** - * 分割線 - */ "divider": string; - /** - * 項目を追加 - */ "addItem": string; - /** - * 並び替え - */ "rearrange": string; - /** - * リレー - */ "relays": string; - /** - * リレーの追加 - */ "addRelay": string; - /** - * inboxのURL - */ "inboxUrl": string; - /** - * 追加済みのリレー - */ "addedRelays": string; - /** - * プッシュ通知を行うには有効にする必要があります。 - */ "serviceworkerInfo": string; - /** - * 削除された投稿 - */ "deletedNote": string; - /** - * 非公開の投稿 - */ "invisibleNote": string; - /** - * 自動でもっと見る - */ "enableInfiniteScroll": string; - /** - * 公開範囲 - */ "visibility": string; - /** - * アンケート - */ "poll": string; - /** - * 内容を隠す - */ "useCw": string; - /** - * プレイヤーを開く - */ "enablePlayer": string; - /** - * プレイヤーを閉じる - */ "disablePlayer": string; - /** - * ポストを展開する - */ "expandTweet": string; - /** - * テーマエディター - */ "themeEditor": string; - /** - * 説明 - */ "description": string; - /** - * キャプションを付ける - */ "describeFile": string; - /** - * キャプションを入力 - */ "enterFileDescription": string; - /** - * 作者 - */ "author": string; - /** - * 未保存の変更があります。破棄しますか? - */ "leaveConfirm": string; - /** - * 管理 - */ "manage": string; - /** - * プラグイン - */ "plugins": string; - /** - * 設定のバックアップ - */ "preferencesBackups": string; - /** - * デッキ - */ "deck": string; - /** - * デッキ解除 - */ "undeck": string; - /** - * モーダルにぼかし効果を使用 - */ "useBlurEffectForModal": string; - /** - * フル機能リアクションピッカーを使用 - */ "useFullReactionPicker": string; - /** - * 幅 - */ "width": string; - /** - * 高さ - */ "height": string; - /** - * 大 - */ "large": string; - /** - * 中 - */ "medium": string; - /** - * 小 - */ "small": string; - /** - * アクセストークンの発行 - */ "generateAccessToken": string; - /** - * 権限 - */ "permission": string; - /** - * 管理者権限 - */ - "adminPermission": string; - /** - * 全て有効にする - */ "enableAll": string; - /** - * 全て無効にする - */ "disableAll": string; - /** - * アカウントへのアクセス許可 - */ "tokenRequested": string; - /** - * このプラグインはここで設定した権限を行使できるようになります。 - */ "pluginTokenRequestedDescription": string; - /** - * 通知の種類 - */ "notificationType": string; - /** - * 編集 - */ "edit": string; - /** - * メールサーバー - */ "emailServer": string; - /** - * メール配信機能を有効化する - */ "enableEmail": string; - /** - * メールアドレスの確認やパスワードリセットの際に使います - */ "emailConfigInfo": string; - /** - * メール - */ "email": string; - /** - * メールアドレス - */ "emailAddress": string; - /** - * SMTP サーバーの設定 - */ "smtpConfig": string; - /** - * ホスト - */ "smtpHost": string; - /** - * ポート - */ "smtpPort": string; - /** - * ユーザー名 - */ "smtpUser": string; - /** - * パスワード - */ "smtpPass": string; - /** - * ユーザー名とパスワードを空欄にすることで、SMTP認証を無効化出来ます - */ "emptyToDisableSmtpAuth": string; - /** - * SMTP 接続に暗黙的なSSL/TLSを使用する - */ "smtpSecure": string; - /** - * STARTTLS使用時はオフにします。 - */ "smtpSecureInfo": string; - /** - * 配信テスト - */ "testEmail": string; - /** - * ワードミュート - */ "wordMute": string; - /** - * ハードワードミュート - */ "hardWordMute": string; - /** - * 正規表現エラー - */ "regexpError": string; - /** - * {tab}ワードミュートの{line}行目の正規表現にエラーが発生しました: - */ - "regexpErrorDescription": ParameterizedString<"tab" | "line">; - /** - * サーバーミュート - */ + "regexpErrorDescription": string; "instanceMute": string; - /** - * {name}が何かを言いました - */ - "userSaysSomething": ParameterizedString<"name">; - /** - * アクティブにする - */ + "userSaysSomething": string; "makeActive": string; - /** - * 表示 - */ "display": string; - /** - * コピー - */ "copy": string; - /** - * メトリクス - */ "metrics": string; - /** - * 概要 - */ "overview": string; - /** - * ログ - */ "logs": string; - /** - * 遅延 - */ "delayed": string; - /** - * データベース - */ "database": string; - /** - * チャンネル - */ "channel": string; - /** - * 作成 - */ "create": string; - /** - * 通知設定 - */ "notificationSetting": string; - /** - * 表示する通知の種別を選択してください。 - */ "notificationSettingDesc": string; - /** - * 通知を全部削除 - */ - "notificationFlush": string; - /** - * グローバル設定を使う - */ "useGlobalSetting": string; - /** - * オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。 - */ "useGlobalSettingDesc": string; - /** - * その他 - */ "other": string; - /** - * ログイントークンを再生成 - */ "regenerateLoginToken": string; - /** - * ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。 - */ "regenerateLoginTokenDescription": string; - /** - * カスタム絵文字を検索する時のキーワードになります。 - */ - "theKeywordWhenSearchingForCustomEmoji": string; - /** - * スペースで区切って複数設定できます。 - */ "setMultipleBySeparatingWithSpace": string; - /** - * ファイルIDまたはURL - */ "fileIdOrUrl": string; - /** - * 動作 - */ "behavior": string; - /** - * サンプル - */ "sample": string; - /** - * 通報 - */ "abuseReports": string; - /** - * 通報 - */ "reportAbuse": string; - /** - * リノートを通報 - */ "reportAbuseRenote": string; - /** - * {name}を通報する - */ - "reportAbuseOf": ParameterizedString<"name">; - /** - * 通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。 - */ + "reportAbuseOf": string; "fillAbuseReportDescription": string; - /** - * 内容が送信されました。ご報告ありがとうございました。 - */ "abuseReported": string; - /** - * 通報者 - */ "reporter": string; - /** - * 通報先 - */ "reporteeOrigin": string; - /** - * 通報元 - */ "reporterOrigin": string; - /** - * リモートサーバーに通報を転送する - */ "forwardReport": string; - /** - * リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。 - */ "forwardReportIsAnonymous": string; - /** - * 送信 - */ "send": string; - /** - * 対応済みにする - */ "abuseMarkAsResolved": string; - /** - * 新しいタブで開く - */ "openInNewTab": string; - /** - * サイドビューで開く - */ "openInSideView": string; - /** - * デフォルトのナビゲーション - */ "defaultNavigationBehaviour": string; - /** - * これらの設定を編集するとアカウントが破損する可能性があります。 - */ "editTheseSettingsMayBreakAccount": string; - /** - * ノートのサーバー情報 - */ "instanceTicker": string; - /** - * {x}を待っています - */ - "waitingFor": ParameterizedString<"x">; - /** - * ランダム - */ + "waitingFor": string; "random": string; - /** - * システム - */ "system": string; - /** - * UI切り替え - */ "switchUi": string; - /** - * デスクトップ - */ "desktop": string; - /** - * クリップ - */ "clip": string; - /** - * 新規作成 - */ "createNew": string; - /** - * 任意 - */ "optional": string; - /** - * 新しいクリップを作成 - */ "createNewClip": string; - /** - * クリップ解除 - */ "unclip": string; - /** - * このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか? - */ - "confirmToUnclipAlreadyClippedNote": ParameterizedString<"name">; - /** - * パブリック - */ + "confirmToUnclipAlreadyClippedNote": string; "public": string; - /** - * 非公開 - */ "private": string; - /** - * CherryPickは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。 - */ - "i18nInfo": ParameterizedString<"link">; - /** - * アクセストークンの管理 - */ + "i18nInfo": string; "manageAccessTokens": string; - /** - * アカウント情報 - */ "accountInfo": string; - /** - * ノートの数 - */ "notesCount": string; - /** - * 返信した数 - */ "repliesCount": string; - /** - * リノートした数 - */ "renotesCount": string; - /** - * 返信された数 - */ "repliedCount": string; - /** - * リノートされた数 - */ "renotedCount": string; - /** - * フォロー数 - */ "followingCount": string; - /** - * フォロワー数 - */ "followersCount": string; - /** - * リアクションした数 - */ "sentReactionsCount": string; - /** - * リアクションされた数 - */ "receivedReactionsCount": string; - /** - * アンケートに投票した数 - */ "pollVotesCount": string; - /** - * アンケートに投票された数 - */ "pollVotedCount": string; - /** - * はい - */ "yes": string; - /** - * いいえ - */ "no": string; - /** - * ドライブのファイル数 - */ "driveFilesCount": string; - /** - * ドライブ使用量 - */ "driveUsage": string; - /** - * クローラーによるインデックスを拒否 - */ "noCrawle": string; - /** - * 外部の検索エンジンにあなたのユーザーページ、ノート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。 - */ "noCrawleDescription": string; - /** - * フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。 - */ "lockedAccountInfo": string; - /** - * デフォルトでメディアをセンシティブ設定にする - */ "alwaysMarkSensitive": string; - /** - * 添付画像のサムネイルをオリジナル画質にする - */ "loadRawImages": string; - /** - * アニメーション画像を再生しない - */ "disableShowingAnimatedImages": string; - /** - * 無効にすると、動く画像が再生されます。光敏感性発作を起こすことがありますので、ご注意ください。 - */ "disableShowingAnimatedImagesDescription": string; - /** - * メディアがセンシティブであることを分かりやすく表示 - */ "highlightSensitiveMedia": string; - /** - * 確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。 - * もしメールが来なかったらスパムメールボックスを確認してください。 - */ "verificationEmailSent": string; - /** - * 未設定 - */ "notSet": string; - /** - * メールアドレスが確認されました - */ "emailVerified": string; - /** - * お気に入りノートの数 - */ "noteFavoritesCount": string; - /** - * Pageにいいねした数 - */ "pageLikesCount": string; - /** - * Pageにいいねされた数 - */ "pageLikedCount": string; - /** - * 連絡先 - */ "contact": string; - /** - * システムのデフォルトのフォントを使う - */ "useSystemFont": string; - /** - * クリップ - */ "clips": string; - /** - * 実験的機能 - */ "experimentalFeatures": string; - /** - * 実験的 - */ "experimental": string; - /** - * これは実験的な機能です。仕様が変更されたり、正常に動作しなかったりする可能性があります。 - */ "thisIsExperimentalFeature": string; - /** - * 開発者 - */ "developer": string; - /** - * アカウントを見つけやすくする - */ "makeExplorable": string; - /** - * オフにすると、「みつける」にアカウントが載らなくなります。 - */ "makeExplorableDescription": string; - /** - * タイムラインのノートを離して表示 - */ "showGapBetweenNotesInTimeline": string; - /** - * 複製 - */ "duplicate": string; - /** - * 左 - */ "left": string; - /** - * 中央 - */ "center": string; - /** - * 広い - */ "wide": string; - /** - * 狭い - */ "narrow": string; - /** - * 設定はページリロード後に反映されます。今すぐリロードしますか? - */ "reloadToApplySetting": string; - /** - * 設定はページリロード後に反映されます。 - */ "reloadToApplySetting2": string; - /** - * 反映には再起動が必要です。 - */ "needReloadToApply": string; - /** - * タイトルバーを表示する - */ "showTitlebar": string; - /** - * キャッシュをクリア - */ "clearCache": string; - /** - * {n}人がオンライン - */ - "onlineUsersCount": ParameterizedString<"n">; - /** - * {n}ユーザー - */ - "nUsers": ParameterizedString<"n">; - /** - * {n}ノート - */ - "nNotes": ParameterizedString<"n">; - /** - * エラーリポートを送信 - */ + "onlineUsersCount": string; + "nUsers": string; + "nNotes": string; "sendErrorReports": string; - /** - * オンにすると、問題が発生したときにエラーの詳細情報がCherryPickに共有され、ソフトウェアの品質向上に役立てることができます。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれます。 - */ "sendErrorReportsDescription": string; - /** - * マイテーマ - */ "myTheme": string; - /** - * 背景 - */ "backgroundColor": string; - /** - * アクセント - */ "accentColor": string; - /** - * 文字 - */ "textColor": string; - /** - * 名前を付けて保存 - */ "saveAs": string; - /** - * 高度 - */ "advanced": string; - /** - * 高度な設定 - */ "advancedSettings": string; - /** - * 値 - */ "value": string; - /** - * 作成日時 - */ "createdAt": string; - /** - * 更新日時 - */ "updatedAt": string; - /** - * 保存しますか? - */ "saveConfirm": string; - /** - * 削除しますか? - */ "deleteConfirm": string; - /** - * 有効な値ではありません。 - */ "invalidValue": string; - /** - * レジストリ - */ "registry": string; - /** - * アカウントを閉鎖する - */ "closeAccount": string; - /** - * 現在のバージョン - */ "currentVersion": string; - /** - * 最新のバージョン - */ "latestVersion": string; - /** - * お使いのクライアントは最新です。 - */ "youAreRunningUpToDateClient": string; - /** - * 新しいバージョンのクライアントが利用可能です。 - */ "newVersionOfClientAvailable": string; - /** - * 使用量 - */ "usageAmount": string; - /** - * 容量 - */ "capacity": string; - /** - * 使用中 - */ "inUse": string; - /** - * コードを編集 - */ "editCode": string; - /** - * 適用 - */ "apply": string; - /** - * サーバーからのお知らせを受け取る - */ "receiveAnnouncementFromInstance": string; - /** - * メール通知 - */ "emailNotification": string; - /** - * 公開 - */ "publish": string; - /** - * チャンネル内検索 - */ "inChannelSearch": string; - /** - * 右クリックでリアクションピッカーを開く - */ "useReactionPickerForContextMenu": string; - /** - * {users}が入力中 - */ - "typingUsers": ParameterizedString<"users">; - /** - * 特定の日付にジャンプ - */ + "typingUsers": string; "jumpToSpecifiedDate": string; - /** - * 過去のタイムラインを表示しています - */ "showingPastTimeline": string; - /** - * クリア - */ "clear": string; - /** - * 全て既読にする - */ "markAllAsRead": string; - /** - * 戻る - */ "goBack": string; - /** - * いいね解除しますか? - */ "unlikeConfirm": string; - /** - * フルビュー - */ "fullView": string; - /** - * フルビュー解除 - */ "quitFullView": string; - /** - * 説明を追加 - */ "addDescription": string; - /** - * 個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。 - */ "userPagePinTip": string; - /** - * 宛先に含まれていないメンションがあります - */ "notSpecifiedMentionWarning": string; - /** - * 情報 - */ "info": string; - /** - * ユーザー情報 - */ "userInfo": string; - /** - * 不明 - */ "unknown": string; - /** - * オンライン状態 - */ "onlineStatus": string; - /** - * オンライン状態を隠す - */ "hideOnlineStatus": string; - /** - * オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。 - */ "hideOnlineStatusDescription": string; - /** - * オンライン - */ "online": string; - /** - * アクティブ - */ "active": string; - /** - * オフライン - */ "offline": string; - /** - * 非推奨 - */ "notRecommended": string; - /** - * Botプロテクション - */ "botProtection": string; - /** - * サーバーブロック・サイレンス - */ "instanceBlocking": string; - /** - * アカウントを選択 - */ "selectAccount": string; - /** - * アカウントを切り替え - */ "switchAccount": string; - /** - * 有効 - */ "enabled": string; - /** - * 無効 - */ "disabled": string; - /** - * クイックアクション - */ "quickAction": string; - /** - * ユーザー - */ "user": string; - /** - * 管理 - */ "administration": string; - /** - * アカウント - */ "accounts": string; - /** - * 切り替え - */ "switch": string; - /** - * 管理者情報が設定されていません。 - */ "noMaintainerInformationWarning": string; - /** - * 問い合わせ先URLが設定されていません。 - */ - "noInquiryUrlWarning": string; - /** - * Botプロテクションが設定されていません。 - */ "noBotProtectionWarning": string; - /** - * 設定する - */ "configure": string; - /** - * ギャラリーへ投稿 - */ "postToGallery": string; - /** - * このハッシュタグで投稿 - */ "postToHashtag": string; - /** - * ギャラリー - */ "gallery": string; - /** - * 最近の投稿 - */ "recentPosts": string; - /** - * 人気の投稿 - */ "popularPosts": string; - /** - * ノートで共有 - */ "shareWithNote": string; - /** - * 広告 - */ "ads": string; - /** - * 期限 - */ "expiration": string; - /** - * 開始期間 - */ "startingperiod": string; - /** - * メモ - */ "memo": string; - /** - * 優先度 - */ "priority": string; - /** - * 高 - */ "high": string; - /** - * 中 - */ "middle": string; - /** - * 低 - */ "low": string; - /** - * メールアドレスの設定がされていません。 - */ "emailNotConfiguredWarning": string; - /** - * 比率 - */ "ratio": string; - /** - * 本文をプレビュー - */ "previewNoteText": string; - /** - * カスタムCSS - */ "customCss": string; - /** - * この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。 - */ "customCssWarn": string; - /** - * グローバル - */ "global": string; - /** - * アイコンを四角形で表示 - */ "squareAvatars": string; - /** - * 送信 - */ "sent": string; - /** - * 受信 - */ "received": string; - /** - * 検索結果 - */ "searchResult": string; - /** - * ハッシュタグ - */ "hashtags": string; - /** - * トラブルシューティング - */ "troubleshooting": string; - /** - * UIにぼかし効果を使用 - */ "useBlurEffect": string; - /** - * 有効にすると、より美しいテーマを使用できます! - */ "useBlurEffectDescription": string; - /** - * 詳しく - */ "learnMore": string; - /** - * CherryPickが更新されました! - */ "misskeyUpdated": string; - /** - * 更新情報を見る - */ "whatIsNew": string; - /** - * 翻訳 - */ "translate": string; - /** - * {x}から翻訳 - */ - "translatedFrom": ParameterizedString<"x">; - /** - * アカウントの削除が進行中です - */ + "translatedFrom": string; "accountDeletionInProgress": string; - /** - * サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。 - */ "usernameInfo": string; - /** - * 藍モード - */ "aiChanMode": string; - /** - * 開発者モード - */ "devMode": string; - /** - * CWを維持する - */ "keepCw": string; - /** - * Pub/Subのアカウント - */ "pubSub": string; - /** - * 直近の通信 - */ "lastCommunication": string; - /** - * 解決済み - */ "resolved": string; - /** - * 未解決 - */ "unresolved": string; - /** - * フォロワーを解除 - */ "breakFollow": string; - /** - * フォロワー解除しますか? - */ "breakFollowConfirm": string; - /** - * オンになっています - */ "itsOn": string; - /** - * オフになっています - */ "itsOff": string; - /** - * オン - */ "on": string; - /** - * オフ - */ "off": string; - /** - * アカウント登録にメールアドレスを必須にする - */ "emailRequiredForSignup": string; - /** - * 未読 - */ "unread": string; - /** - * フィルタ - */ "filter": string; - /** - * コントロールパネル - */ "controlPanel": string; - /** - * アカウントを管理 - */ "manageAccounts": string; - /** - * リアクション一覧を公開する - */ "makeReactionsPublic": string; - /** - * あなたがしたリアクション一覧を誰でも見れるようにします。 - */ "makeReactionsPublicDescription": string; - /** - * クラシック - */ "classic": string; - /** - * スレッドをミュート - */ "muteThread": string; - /** - * スレッドのミュートを解除 - */ "unmuteThread": string; - /** - * フォローの公開範囲 - */ "followingVisibility": string; - /** - * フォロワーの公開範囲 - */ "followersVisibility": string; - /** - * さらにスレッドを見る - */ "continueThread": string; - /** - * アカウントが削除されます。よろしいですか? - */ "deleteAccountConfirm": string; - /** - * パスワードが間違っています。 - */ "incorrectPassword": string; - /** - * 「{choice}」に投票しますか? - */ - "voteConfirm": ParameterizedString<"choice">; - /** - * 隠す - */ + "voteConfirm": string; "hide": string; - /** - * グループから抜ける - */ "leaveGroup": string; - /** - * 「{name}」から抜けますか? - */ - "leaveGroupConfirm": ParameterizedString<"name">; - /** - * モバイルデバイスのときドロワーで表示 - */ + "leaveGroupConfirm": string; "useDrawerReactionPickerForMobile": string; - /** - * おかえりなさい、{name}さん - */ - "welcomeBackWithName": ParameterizedString<"name">; - /** - * [{ok}]を押して、メールアドレスの確認を完了してください。 - */ - "clickToFinishEmailVerification": ParameterizedString<"ok">; - /** - * デバイスタイプ - */ + "welcomeBackWithName": string; + "clickToFinishEmailVerification": string; "overridedDeviceKind": string; - /** - * スマートフォン - */ "smartphone": string; - /** - * タブレット - */ "tablet": string; - /** - * 自動 - */ "auto": string; - /** - * テーマカラー - */ "themeColor": string; - /** - * サイズ - */ "size": string; - /** - * 列の数 - */ "numberOfColumn": string; - /** - * 検索 - */ "searchByGoogle": string; - /** - * サーバーデフォルトのライトテーマ - */ "instanceDefaultLightTheme": string; - /** - * サーバーデフォルトのダークテーマ - */ "instanceDefaultDarkTheme": string; - /** - * オブジェクト形式のテーマコードを記入します。 - */ "instanceDefaultThemeDescription": string; - /** - * ミュートする期限 - */ "mutePeriod": string; - /** - * 期限 - */ "period": string; - /** - * 無期限 - */ "indefinitely": string; - /** - * 10分 - */ "tenMinutes": string; - /** - * 1時間 - */ "oneHour": string; - /** - * 1日 - */ "oneDay": string; - /** - * 1週間 - */ "oneWeek": string; - /** - * 1ヶ月 - */ "oneMonth": string; - /** - * 反映されるまで時間がかかる場合があります。 - */ "reflectMayTakeTime": string; - /** - * アカウント情報の取得に失敗しました - */ "failedToFetchAccountInformation": string; - /** - * レート制限を超えました - */ "rateLimitExceeded": string; - /** - * 画像のクロップ - */ "cropImage": string; - /** - * 画像をクロップしますか? - */ "cropImageAsk": string; - /** - * クロップする - */ "cropYes": string; - /** - * そのまま使う - */ "cropNo": string; - /** - * ファイル - */ "file": string; - /** - * 直近{n}時間 - */ - "recentNHours": ParameterizedString<"n">; - /** - * 直近{n}日 - */ - "recentNDays": ParameterizedString<"n">; - /** - * メールサーバーの設定がされていません。 - */ + "recentNHours": string; + "recentNDays": string; "noEmailServerWarning": string; - /** - * 未対応の通報があります。 - */ "thereIsUnresolvedAbuseReportWarning": string; - /** - * 推奨 - */ "recommended": string; - /** - * チェック - */ "check": string; - /** - * このユーザーのドライブ容量上限を変更 - */ "driveCapOverrideLabel": string; - /** - * 0以下を指定すると解除されます。 - */ "driveCapOverrideCaption": string; - /** - * 閲覧するには管理者アカウントでログインしている必要があります。 - */ "requireAdminForView": string; - /** - * システムにより自動で作成・管理されているアカウントです。 - */ "isSystemAccount": string; - /** - * この操作を行うには {x} と入力してください - */ - "typeToConfirm": ParameterizedString<"x">; - /** - * アカウント削除 - */ + "typeToConfirm": string; "deleteAccount": string; - /** - * ドキュメント - */ "document": string; - /** - * ページキャッシュ数 - */ "numberOfPageCache": string; - /** - * 多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。 - */ "numberOfPageCacheDescription": string; - /** - * ログアウトしますか? - */ "logoutConfirm": string; - /** - * 最終利用日時 - */ "lastActiveDate": string; - /** - * ステータスバー - */ "statusbar": string; - /** - * 選択してください - */ "pleaseSelect": string; - /** - * 反転 - */ "reverse": string; - /** - * 色付き - */ "colored": string; - /** - * 更新間隔 - */ "refreshInterval": string; - /** - * ラベル - */ "label": string; - /** - * タイプ - */ "type": string; - /** - * 速度 - */ "speed": string; - /** - * 遅い - */ "slow": string; - /** - * 速い - */ "fast": string; - /** - * センシティブなメディアの検出 - */ "sensitiveMediaDetection": string; - /** - * ローカルのみ - */ "localOnly": string; - /** - * リモートのみ - */ "remoteOnly": string; - /** - * アップロード失敗 - */ "failedToUpload": string; - /** - * 不適切な内容を含む可能性があると判定されたためアップロードできません。 - */ "cannotUploadBecauseInappropriate": string; - /** - * ドライブの空き容量が無いためアップロードできません。 - */ "cannotUploadBecauseNoFreeSpace": string; - /** - * ファイルサイズの制限を超えているためアップロードできません。 - */ "cannotUploadBecauseExceedsFileSizeLimit": string; - /** - * ベータ - */ "beta": string; - /** - * 自動センシティブ判定 - */ "enableAutoSensitive": string; - /** - * 利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。 - */ "enableAutoSensitiveDescription": string; - /** - * ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。 - */ "activeEmailValidationDescription": string; - /** - * ナビゲーションバー - */ "navbar": string; - /** - * シャッフル - */ "shuffle": string; - /** - * アカウント - */ "account": string; - /** - * 移動 - */ "move": string; - /** - * プッシュ通知 - */ "pushNotification": string; - /** - * プッシュ通知を有効化 - */ "subscribePushNotification": string; - /** - * プッシュ通知を停止する - */ "unsubscribePushNotification": string; - /** - * プッシュ通知は有効です - */ "pushNotificationAlreadySubscribed": string; - /** - * ブラウザかサーバーがプッシュ通知に非対応 - */ "pushNotificationNotSupported": string; - /** - * 通知が既読になったらプッシュ通知を削除する - */ "sendPushNotificationReadMessage": string; - /** - * 端末の電池消費量が増加する可能性があります。 - */ "sendPushNotificationReadMessageCaption": string; - /** - * 最大化 - */ "windowMaximize": string; - /** - * 最小化 - */ "windowMinimize": string; - /** - * 元に戻す - */ "windowRestore": string; - /** - * キャプション - */ "caption": string; - /** - * Botアカウントでログイン中 - */ "loggedInAsBot": string; - /** - * ツール - */ "tools": string; - /** - * 読み込めません - */ "cannotLoad": string; - /** - * プロフィール表示回数 - */ "numberOfProfileView": string; - /** - * いいね! - */ "like": string; - /** - * いいねを解除 - */ "unlike": string; - /** - * いいね数 - */ "numberOfLikes": string; - /** - * 表示 - */ "show": string; - /** - * 今後表示しない - */ "neverShow": string; - /** - * また後で - */ "remindMeLater": string; - /** - * CherryPickを気に入っていただけましたか? - */ "didYouLikeMisskey": string; - /** - * CherryPickは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします! - */ - "pleaseDonate": ParameterizedString<"host">; - /** - * 対応するソースコードは{anchor}から利用可能です。 - */ - "correspondingSourceIsAvailable": ParameterizedString<"anchor">; - /** - * ロール - */ + "pleaseDonate": string; "roles": string; - /** - * ロール - */ "role": string; - /** - * ロールはありません - */ "noRole": string; - /** - * 一般ユーザー - */ "normalUser": string; - /** - * 未定義 - */ "undefined": string; - /** - * アサイン - */ "assign": string; - /** - * アサインを解除 - */ "unassign": string; - /** - * 色 - */ "color": string; - /** - * カスタム絵文字の管理 - */ "manageCustomEmojis": string; - /** - * アバターデコレーションの管理 - */ "manageAvatarDecorations": string; - /** - * これ以上作成することはできません。 - */ "youCannotCreateAnymore": string; - /** - * 一時的に利用できません - */ "cannotPerformTemporary": string; - /** - * 操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。 - */ "cannotPerformTemporaryDescription": string; - /** - * パラメータエラー - */ "invalidParamError": string; - /** - * リクエストパラメータに問題があります。通常これはバグですが、入力した文字数が多すぎる等の可能性もあります。 - */ "invalidParamErrorDescription": string; - /** - * 操作が拒否されました - */ "permissionDeniedError": string; - /** - * このアカウントにはこの操作を行うための権限がありません。 - */ "permissionDeniedErrorDescription": string; - /** - * プリセット - */ "preset": string; - /** - * プリセットから選択 - */ "selectFromPresets": string; - /** - * 実績 - */ "achievements": string; - /** - * サーバーの応答が無効です - */ "gotInvalidResponseError": string; - /** - * サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。 - */ "gotInvalidResponseErrorDescription": string; - /** - * この投稿は迷惑になる可能性があります。 - */ "thisPostMayBeAnnoying": string; - /** - * ホームに投稿 - */ "thisPostMayBeAnnoyingHome": string; - /** - * やめる - */ "thisPostMayBeAnnoyingCancel": string; - /** - * このまま投稿 - */ "thisPostMayBeAnnoyingIgnore": string; - /** - * リノートのスマート省略 - */ "collapseRenotes": string; - /** - * リアクションやリノートをしたことがあるノートをたたんで表示します。 - */ - "collapseRenotesDescription": string; - /** - * 特定のMFM構文を含むノートを省略して表示 - */ "collapseDefault": string; - /** - * サーバー内部エラー - */ "internalServerError": string; - /** - * サーバー内部で予期しないエラーが発生しました。 - */ "internalServerErrorDescription": string; - /** - * エラー情報をコピー - */ "copyErrorInfo": string; - /** - * このサーバーに登録する - */ "joinThisServer": string; - /** - * 他のサーバーを探す - */ "exploreOtherServers": string; - /** - * タイムラインを見てみる - */ "letsLookAtTimeline": string; - /** - * 連合なしにしますか? - */ "disableFederationConfirm": string; - /** - * 連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。 - */ "disableFederationConfirmWarn": string; - /** - * 連合なしにする - */ "disableFederationOk": string; - /** - * 現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。 - */ "invitationRequiredToRegister": string; - /** - * このサーバーではメール配信はサポートされていません - */ "emailNotSupported": string; - /** - * チャンネルに投稿 - */ "postToTheChannel": string; - /** - * 後から変更できません。 - */ "cannotBeChangedLater": string; - /** - * リアクションの受け入れ - */ "reactionAcceptance": string; - /** - * いいねのみ - */ "likeOnly": string; - /** - * 全て (リモートはいいねのみ) - */ "likeOnlyForRemote": string; - /** - * 非センシティブのみ - */ "nonSensitiveOnly": string; - /** - * 非センシティブのみ (リモートはいいねのみ) - */ "nonSensitiveOnlyForLocalLikeOnlyForRemote": string; - /** - * 自分に割り当てられたロール - */ "rolesAssignedToMe": string; - /** - * パスワードリセットしますか? - */ "resetPasswordConfirm": string; - /** - * センシティブワード - */ "sensitiveWords": string; - /** - * 設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。 - */ "sensitiveWordsDescription": string; - /** - * スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。 - */ "sensitiveWordsDescription2": string; - /** - * 禁止ワード - */ - "prohibitedWords": string; - /** - * 設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。 - */ - "prohibitedWordsDescription": string; - /** - * スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。 - */ - "prohibitedWordsDescription2": string; - /** - * 非表示ハッシュタグ - */ "hiddenTags": string; - /** - * 設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。 - */ "hiddenTagsDescription": string; - /** - * ノート検索は利用できません。 - */ "notesSearchNotAvailable": string; - /** - * ライセンス - */ "license": string; - /** - * お気に入り解除しますか? - */ "unfavoriteConfirm": string; - /** - * 自分のクリップ - */ "myClips": string; - /** - * ドライブクリーナー - */ "drivecleaner": string; - /** - * すべてのキューを今すぐ再試行 - */ "retryAllQueuesNow": string; - /** - * 今すぐ再試行しますか? - */ "retryAllQueuesConfirmTitle": string; - /** - * 一時的にサーバーの負荷が増大することがあります。 - */ "retryAllQueuesConfirmText": string; - /** - * リモートユーザーのチャートを生成 - */ "enableChartsForRemoteUser": string; - /** - * リモートサーバーのチャートを生成 - */ "enableChartsForFederatedInstances": string; - /** - * ノートのアクションにクリップを追加 - */ "showClipButtonInNoteFooter": string; - /** - * リアクションの表示サイズ - */ "reactionsDisplaySize": string; - /** - * リアクションの最大横幅を制限し、縮小して表示する - */ "limitWidthOfReaction": string; - /** - * ノートIDまたはURL - */ "noteIdOrUrl": string; - /** - * 動画 - */ "video": string; - /** - * 動画 - */ "videos": string; - /** - * 音声 - */ - "audio": string; - /** - * 音声 - */ - "audioFiles": string; - /** - * データセーバー - */ "dataSaver": string; - /** - * アカウントの移行 - */ "accountMigration": string; - /** - * このユーザーは新しいアカウントに移行しました: - */ "accountMoved": string; - /** - * このアカウントは移行されています - */ "accountMovedShort": string; - /** - * この操作はできません - */ "operationForbidden": string; - /** - * 常に広告を表示する - */ "forceShowAds": string; - /** - * イベント - */ "event": string; - /** - * イベント - */ "events": string; - /** - * 終了済み - */ "reverseChronological": string; - /** - * メモを追加 - */ "addMemo": string; - /** - * メモを編集 - */ "editMemo": string; - /** - * リアクション一覧 - */ "reactionsList": string; - /** - * リノート一覧 - */ "renotesList": string; - /** - * 通知の表示 - */ "notificationDisplay": string; - /** - * 左上 - */ "leftTop": string; - /** - * 右上 - */ "rightTop": string; - /** - * 左下 - */ "leftBottom": string; - /** - * 右下 - */ "rightBottom": string; - /** - * スタック方向 - */ "stackAxis": string; - /** - * 縦 - */ "vertical": string; - /** - * 横 - */ "horizontal": string; - /** - * 位置 - */ "position": string; - /** - * サーバールール - */ "serverRules": string; - /** - * このサーバーに登録するには、以下の内容を確認し同意する必要があります。 - */ "pleaseConfirmBelowBeforeSignup": string; - /** - * 続けるには、全ての「同意する」にチェックが入っている必要があります。 - */ "pleaseAgreeAllToContinue": string; - /** - * 続ける - */ "continue": string; - /** - * 予約ユーザー名 - */ "preservedUsernames": string; - /** - * 予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。 - */ "preservedUsernamesDescription": string; - /** - * このファイルからノートを作成 - */ "createNoteFromTheFile": string; - /** - * アーカイブ - */ "archive": string; - /** - * アーカイブ済み - */ - "archived": string; - /** - * アーカイブ解除 - */ - "unarchive": string; - /** - * {name}をアーカイブしますか? - */ - "channelArchiveConfirmTitle": ParameterizedString<"name">; - /** - * アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。 - */ + "channelArchiveConfirmTitle": string; "channelArchiveConfirmDescription": string; - /** - * このチャンネルはアーカイブされています。 - */ "thisChannelArchived": string; - /** - * ノートの表示 - */ "displayOfNote": string; - /** - * 初期設定 - */ "initialAccountSetting": string; - /** - * フォロー中 - */ "youFollowing": string; - /** - * 生成AIによる学習を拒否 - */ "preventAiLearning": string; - /** - * 外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。 - */ "preventAiLearningDescription": string; - /** - * オプション - */ "options": string; - /** - * ユーザー指定 - */ "specifyUser": string; - /** - * 照会しますか? - */ - "lookupConfirm": string; - /** - * ハッシュタグのページを開きますか? - */ - "openTagPageConfirm": string; - /** - * ホスト指定 - */ - "specifyHost": string; - /** - * プレビューできません - */ "failedToPreviewUrl": string; - /** - * 更新 - */ "update": string; - /** - * リアクションとして使えるロール - */ "rolesThatCanBeUsedThisEmojiAsReaction": string; - /** - * ロールの指定が一つもない場合、誰でもリアクションとして使えます。 - */ "rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription": string; - /** - * ロールは公開ロールである必要があります。 - */ "rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn": string; - /** - * リアクションを取り消しますか? - */ "cancelReactionConfirm": string; - /** - * リアクションを変更しますか? - */ "changeReactionConfirm": string; - /** - * あとで - */ "later": string; - /** - * CherryPickへ - */ "goToMisskey": string; - /** - * 絵文字の追加辞書 - */ "additionalEmojiDictionary": string; - /** - * インストール済み - */ "installed": string; - /** - * ブランディング - */ "branding": string; - /** - * サーバーのマシン情報を公開する - */ "enableServerMachineStats": string; - /** - * ユーザーごとのIdenticon生成を有効にする - */ "enableIdenticonGeneration": string; - /** - * オフにするとパフォーマンスが向上します。 - */ "turnOffToImprovePerformance": string; - /** - * 招待コードを作成 - */ "createInviteCode": string; - /** - * オプションを指定して作成 - */ "createWithOptions": string; - /** - * 作成数 - */ "createCount": string; - /** - * 招待コードを作成しました - */ "inviteCodeCreated": string; - /** - * 作成できる招待コードの数が上限に達しています。 - */ "inviteLimitExceeded": string; - /** - * 作成できる招待コード: 残り {limit} 個 - */ - "createLimitRemaining": ParameterizedString<"limit">; - /** - * {time}で最大 {limit} 個の招待コードを作成できます。 - */ - "inviteLimitResetCycle": ParameterizedString<"time" | "limit">; - /** - * 有効期限 - */ + "createLimitRemaining": string; + "inviteLimitResetCycle": string; "expirationDate": string; - /** - * 有効期限を設けない - */ "noExpirationDate": string; - /** - * 招待コードが使用された日時 - */ "inviteCodeUsedAt": string; - /** - * 招待コードを使用したユーザー - */ "registeredUserUsingInviteCode": string; - /** - * メール認証待ち - */ "waitingForMailAuth": string; - /** - * 招待コードを作成したユーザー - */ "inviteCodeCreator": string; - /** - * 使用日時 - */ "usedAt": string; - /** - * 未使用 - */ "unused": string; - /** - * 使用済み - */ "used": string; - /** - * 期限切れ - */ "expired": string; - /** - * 同意しますか? - */ "doYouAgree": string; - /** - * 重要ですので必ずお読みください。 - */ "beSureToReadThisAsItIsImportant": string; - /** - * 「{x}」の内容をよく読み、同意します。 - */ - "iHaveReadXCarefullyAndAgree": ParameterizedString<"x">; - /** - * 通報の通知メールを発送しないようにする - */ + "iHaveReadXCarefullyAndAgree": string; "doNotSendNotificationEmailsForAbuseReport": string; - /** - * 通報通知を受け取るためのメールアドレス - */ "emailToReceiveAbuseReport": string; - /** - * 通報通知を受け取るためのメールアドレスを指定します。ここの入力欄を空にするとメールサーバーのメールアドレスが使用されます。 - */ "emailToReceiveAbuseReportCaption": string; - /** - * ダイアログ - */ "dialog": string; - /** - * アイコン - */ "icon": string; - /** - * あなたへ - */ "forYou": string; - /** - * 現在のお知らせ - */ "currentAnnouncements": string; - /** - * 過去のお知らせ - */ "pastAnnouncements": string; - /** - * 未読のお知らせがあります。 - */ "youHaveUnreadAnnouncements": string; - /** - * ブラウザまたはデバイスの指示に従って、セキュリティキーまたはパスキーを使用してください。 - */ "useSecurityKey": string; - /** - * 返信 - */ "replies": string; - /** - * リノート - */ "renotes": string; - /** - * 返信を見る - */ "loadReplies": string; - /** - * 会話を見る - */ "loadConversation": string; - /** - * ピン留めされたリスト - */ "pinnedList": string; - /** - * デバイスの画面を常にオンにする - */ "keepScreenOn": string; - /** - * このリンク先の所有者であることが確認されました - */ "verifiedLink": string; - /** - * 投稿を通知 - */ "notifyNotes": string; - /** - * 投稿の通知を解除 - */ "unnotifyNotes": string; - /** - * 認証 - */ "authentication": string; - /** - * 続けるには認証を行ってください - */ "authenticationRequiredToContinue": string; - /** - * 日時 - */ "dateAndTime": string; - /** - * リノートを表示 - */ "showRenotes": string; - /** - * 編集済み - */ "edited": string; - /** - * 通知の受信設定 - */ "notificationRecieveConfig": string; - /** - * 相互フォロー - */ "mutualFollow": string; - /** - * フォロー中またはフォロワー - */ - "followingOrFollower": string; - /** - * ファイル付きのみ - */ "fileAttachedOnly": string; - /** - * TLに他の人への返信を含める - */ "showRepliesToOthersInTimeline": string; - /** - * TLに他の人への返信を含めない - */ "hideRepliesToOthersInTimeline": string; - /** - * TLに現在フォロー中の人全員の返信を含めるようにする - */ "showRepliesToOthersInTimelineAll": string; - /** - * TLに現在フォロー中の人全員の返信を含めないようにする - */ "hideRepliesToOthersInTimelineAll": string; - /** - * この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか? - */ "confirmShowRepliesAll": string; - /** - * この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか? - */ "confirmHideRepliesAll": string; - /** - * 外部サービス - */ "externalServices": string; - /** - * ソースコード - */ - "sourceCode": string; - /** - * ソースコードはまだ提供されていません。この問題の修正について管理者に問い合わせてください。 - */ - "sourceCodeIsNotYetProvided": string; - /** - * リポジトリURL - */ - "repositoryUrl": string; - /** - * ソースコードが公開されているリポジトリがある場合、そのURLを記入します。CherryPickを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/kokonect-link/cherrypick と記入します。 - */ - "repositoryUrlDescription": string; - /** - * リポジトリを公開していない場合、代わりにtarballを提供する必要があります。詳細は.config/example.ymlを参照してください。 - */ - "repositoryUrlOrTarballRequired": string; - /** - * フィードバック - */ - "feedback": string; - /** - * フィードバックURL - */ - "feedbackUrl": string; - /** - * 運営者情報 - */ "impressum": string; - /** - * 運営者情報URL - */ "impressumUrl": string; - /** - * ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。 - */ "impressumDescription": string; - /** - * プライバシーポリシー - */ "privacyPolicy": string; - /** - * プライバシーポリシーURL - */ "privacyPolicyUrl": string; - /** - * 利用規約・プライバシーポリシー - */ "tosAndPrivacyPolicy": string; - /** - * ステータスページ - */ - "statusUrl": string; - /** - * アイコンデコレーション - */ "avatarDecorations": string; - /** - * 付ける - */ "attach": string; - /** - * 外す - */ "detach": string; - /** - * 全て外す - */ "detachAll": string; - /** - * 角度 - */ "angle": string; - /** - * 反転 - */ "flip": string; - /** - * アイコンのデコレーションを表示 - */ "showAvatarDecorations": string; - /** - * 離してリロード - */ "releaseToRefresh": string; - /** - * リロード中 - */ "refreshing": string; - /** - * 引っ張ってリロード - */ "pullDownToRefresh": string; - /** - * タイムラインのリアルタイム更新を無効にする - */ "disableStreamingTimeline": string; - /** - * 通知をグルーピングして表示する - */ "useGroupedNotifications": string; - /** - * メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。 - */ "signupPendingError": string; - /** - * 「内容を隠す」がオンの場合は注釈の記述が必要です。 - */ "cwNotationRequired": string; - /** - * リアクションする - */ "doReaction": string; - /** - * コード - */ "code": string; - /** - * 設定の反映にはリロードが必要です。 - */ "reloadRequiredToApplySettings": string; - /** - * 残り: {n} - */ - "remainingN": ParameterizedString<"n">; - /** - * 現在の内容に上書きされますがよろしいですか? - */ + "remainingN": string; "overwriteContentConfirm": string; - /** - * 季節に応じた画面の演出 - */ "seasonalScreenEffect": string; - /** - * デコる - */ "decorate": string; - /** - * 装飾を追加 - */ "addMfmFunction": string; - /** - * 高度なMFMのピッカーを表示する - */ "enableQuickAddMfmFunction": string; - /** - * バブルゲーム - */ - "bubbleGame": string; - /** - * 効果音 - */ - "sfx": string; - /** - * サウンドが再生されます - */ - "soundWillBePlayed": string; - /** - * リプレイを見る - */ - "showReplay": string; - /** - * リプレイ - */ - "replay": string; - /** - * リプレイ中 - */ - "replaying": string; - /** - * リプレイを終了 - */ - "endReplay": string; - /** - * リプレイデータをコピー - */ - "copyReplayData": string; - /** - * ランキング - */ - "ranking": string; - /** - * 直近{n}日 - */ - "lastNDays": ParameterizedString<"n">; - /** - * タイトルへ - */ - "backToTitle": string; - /** - * お住まいの地域 - */ - "hemisphere": string; - /** - * センシティブなファイルを含むノートを表示 - */ - "withSensitive": string; - /** - * {name}のセンシティブなファイルを含む投稿 - */ - "userSaysSomethingSensitive": ParameterizedString<"name">; - /** - * スワイプしてタブを切り替える - */ - "enableHorizontalSwipe": string; - /** - * 読み込み中 - */ - "loading": string; - /** - * やめる - */ - "surrender": string; - /** - * リトライ - */ - "gameRetry": string; - /** - * 使用しない場合は空欄にしてください - */ - "notUsePleaseLeaveBlank": string; - /** - * ワンタイムパスワードを使う - */ - "useTotp": string; - /** - * バックアップコードを使う - */ - "useBackupCode": string; - /** - * アプリを起動 - */ - "launchApp": string; - /** - * 動画・音声の再生にブラウザのUIを使用する - */ - "useNativeUIForVideoAudioPlayer": string; - /** - * オリジナルのファイル名を保持 - */ - "keepOriginalFilename": string; - /** - * この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。 - */ - "keepOriginalFilenameDescription": string; - /** - * 説明文はありません - */ - "noDescription": string; - /** - * フォローの際常に確認する - */ - "alwaysConfirmFollow": string; - /** - * お問い合わせ - */ - "inquiry": string; - /** - * もう一度お試しください。 - */ - "tryAgain": string; - /** - * センシティブなメディアを表示するとき確認する - */ - "confirmWhenRevealingSensitiveMedia": string; - /** - * センシティブなメディアです。表示しますか? - */ - "sensitiveMediaRevealConfirm": string; - /** - * 作成したリスト - */ - "createdLists": string; - /** - * 作成したアンテナ - */ - "createdAntennas": string; - /** - * 未読の通知の数を表示する - */ "showUnreadNotificationsCount": string; - /** - * ネコミミ付きのみ - */ "showCatOnly": string; - /** - * Playへの追加許可 - */ "additionalPermissionsForFlash": string; - /** - * このPlayは以下の権限を要求しています - */ "thisFlashRequiresTheFollowingPermissions": string; - /** - * このPlayによるアカウントへのアクセスを許可しますか? - */ "doYouWantToAllowThisPlayToAccessYourAccount": string; - /** - * プロフィールを翻訳する - */ "translateProfile": string; - /** - * QRコードを取得 - */ - "getQrCode": string; "_nsfwOpenBehavior": { - /** - * タップして開く - */ "click": string; - /** - * 二回タップして開く - */ "doubleClick": string; }; "_vibrations": { - /** - * 要素をクリックしたとき - */ "click": string; - /** - * タイムラインに新しいノートがあるとき - */ "note": string; - /** - * 通知を受け取ったとき - */ "notification": string; - /** - * チャット - */ "chat": string; - /** - * チャット(バックグラウンド) - */ "chatBg": string; - /** - * システムの触覚 - */ "system": string; }; "_showingAnimatedImages": { - /** - * 常に再生 - */ "always": string; - /** - * インタラクト時に再生 - */ "interaction": string; - /** - * 一定時間経過すると再生 - */ "inactive": string; }; "_messaging": { - /** - * ダイレクトメッセージ - */ "direct": string; }; - "_delivery": { - /** - * 配信状態 - */ - "status": string; - /** - * 配信停止 - */ - "stop": string; - /** - * 配信再開 - */ - "resume": string; - "_type": { - /** - * 配信中 - */ - "none": string; - /** - * 手動停止中 - */ - "manuallySuspended": string; - /** - * サーバー削除のため停止中 - */ - "goneSuspended": string; - /** - * サーバー応答なしのため停止中 - */ - "autoSuspendedForNotResponding": string; - }; - }; - "_bubbleGame": { - /** - * 遊び方 - */ - "howToPlay": string; - /** - * ホールド - */ - "hold": string; - "_score": { - /** - * スコア - */ - "score": string; - /** - * 稼いだ金額 - */ - "scoreYen": string; - /** - * ハイスコア - */ - "highScore": string; - /** - * 最大チェーン数 - */ - "maxChain": string; - /** - * {yen}円 - */ - "yen": ParameterizedString<"yen">; - /** - * {qty}個分 - */ - "estimatedQty": ParameterizedString<"qty">; - /** - * おにぎり {onigiriQtyWithUnit} - */ - "scoreSweets": ParameterizedString<"onigiriQtyWithUnit">; - }; - "_howToPlay": { - /** - * 位置を調整してハコにモノを落とします。 - */ - "section1": string; - /** - * 同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。 - */ - "section2": string; - /** - * モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう! - */ - "section3": string; - }; - }; "_announcement": { - /** - * 既存ユーザーのみ - */ "forExistingUsers": string; - /** - * 有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。 - */ "forExistingUsersDescription": string; - /** - * 既読にするのに確認が必要 - */ "needConfirmationToRead": string; - /** - * 有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象になりません。 - */ "needConfirmationToReadDescription": string; - /** - * お知らせを終了 - */ "end": string; - /** - * アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。 - */ "tooManyActiveAnnouncementDescription": string; - /** - * 既読にしますか? - */ "readConfirmTitle": string; - /** - * 「{title}」の内容を読み、既読にします。 - */ - "readConfirmText": ParameterizedString<"title">; - /** - * 特に新規ユーザーのUXを損ねる可能性が高いため、常時掲示するための情報ではなく、即時性が求められる情報の掲示のためにお知らせを使用することを推奨します。 - */ + "readConfirmText": string; "shouldNotBeUsedToPresentPermanentInfo": string; - /** - * ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。 - */ "dialogAnnouncementUxWarn": string; - /** - * 非通知 - */ "silence": string; - /** - * オンにすると、このお知らせは通知されず、既読にする必要もなくなります。 - */ "silenceDescription": string; }; "_group": { - /** - * グループオーナー - */ "leader": string; - /** - * 追放 - */ "banish": string; - /** - * 本当に"{name}"さんを"{group}"から追放しますか? - */ - "banishConfirm": ParameterizedString<"name" | "group">; + "banishConfirm": string; }; "_cherrypick": { - /** - * 独自機能 - */ "function": string; - /** - * CherryPickが追加する独自機能を有効・無効にします。 - */ "functionDescription": string; - /** - * ニックネーム機能 - */ "nickname": string; - /** - * ユーザーページにて、ユーザーの名前をクリック/タップすることで好きなものに変更できるようになります。変更は自分にのみ反映されます。 - * 頻繁に名前を変更するユーザーを識別するときなどに使えます。 - */ "nicknameDescription": string; - /** - * Enterキーを押して送信 - */ "useEnterToSend": string; - /** - * オプションを有効にすると、行替えはShift+Enterキーでできます。チャットを送信するときはオプションの影響を受けません。 - */ "useEnterToSendDescription": string; - /** - * ショートカットキーで公開範囲を切り替える - */ "postFormVisibilityHotkey": string; - /** - * ノートを作成する際、Ctrl(control) + Shiftキーを押すと公開範囲を切り替えることができます。ローカルのみショートカットキーは、Ctrl(commandまたはcontrol) + Alt(option)キーです。 - */ "postFormVisibilityHotkeyDescription": string; - /** - * Renoteするときに確認ポップアップを表示 - */ "showRenoteConfirmPopup": string; - /** - * この設定は「全般 - リノートと引用ボタンを分けて表示する」設定がオンになっている必要があります。 - */ "showRenoteConfirmPopupDescription": string; - /** - * クリックでノートの詳細を開く - */ "expandOnNoteClick": string; - /** - * オフの場合、ノートメニューの[詳細]をクリックするか、日付をクリックして開けます。 - */ "expandOnNoteClickDescription": string; - /** - * ノートをクリックして開くとき - */ - "expandOnNoteClickBehavior": string; - "_expandOnNoteClickBehavior": { - /** - * クリックして開く - */ - "click": string; - /** - * ダブルクリックで開く - */ - "doubleClick": string; - }; - /** - * スクロール時の要素表示(ヘッダー、フローティングボタン、ナビゲーションバー) - */ "displayHeaderNavBarWhenScroll": string; "_displayHeaderNavBarWhenScroll": { - /** - * 全て表示 - */ "all": string; - /** - * ヘッダーだけを隠す - */ "hideHeaderOnly": string; - /** - * ヘッダーとフローティングボタンを隠す - */ "hideHeaderFloatBtn": string; - /** - * フローティングボタンだけを隠す - */ "hideFloatBtnOnly": string; - /** - * フローティングボタンとナビゲーションバーを隠す - */ "hideFloatBtnNavBar": string; - /** - * 全て隠す - */ "hide": string; }; - /** - * パッチ - */ "patch": string; - /** - * Misskeyの機能に変更を加えます。 - */ "patchDescription": string; - /** - * リモートのカスタム絵文字リアクションでも、このサーバーに同じ名前の絵文字があればリアクションできるようにする - */ "reactableRemoteReaction": string; - /** - * 既にフォローしている場合、通知欄にフォローボタンを表示しない - */ "showFollowingMessageInsteadOfButton": string; - /** - * モバイル環境でヘッダーのデザインを変更 - */ "mobileHeaderChange": string; - /** - * ノート作成画面の「ノート」ボタンを「にゃ!」に変更する - */ "renameTheButtonInPostFormToNya": string; - /** - * にゃあにゃんにゃんにゃんにゃにゃん? - */ "renameTheButtonInPostFormToNyaDescription": string; - /** - * 長押しでアカウントメニューを開く - */ "enableLongPressOpenAccountMenu": string; - /** - * 画面下部のタイムラインタブを長押しして開くことができます。 - */ "enableLongPressOpenAccountMenuDescription": string; - /** - * フローティングボタンにアイコンのデコレーションを表示 - */ "friendlyShowAvatarDecorationsInNavBtn": string; }; "_bannerDisplay": { - /** - * 全て - */ "all": string; - /** - * 上部と下部 - */ "topBottom": string; - /** - * 上部(サーバーバナー) - */ "top": string; - /** - * 下部(プロフィールバナー) - */ "bottom": string; - /** - * バックグラウンド - */ "bg": string; - /** - * 隠す - */ "hide": string; }; "_requireRefreshBehavior": { - /** - * ダイアログで通知 - */ "dialog": string; - /** - * 控えめに通知 - */ "quiet": string; }; "_initialAccountSetting": { - /** - * アカウントの作成が完了しました! - */ "accountCreated": string; - /** - * さっそくアカウントの初期設定を行いましょう。 - */ "letsStartAccountSetup": string; - /** - * まずはあなたのプロフィールを設定しましょう。 - */ "letsFillYourProfile": string; - /** - * プロフィール設定 - */ "profileSetting": string; - /** - * プライバシー設定 - */ "privacySetting": string; - /** - * フォントサイズ設定 - */ "fontSizeSetting": string; - /** - * ぼかし効果設定 - */ "blurEffectsSetting": string; - /** - * MFMとアニメーション画像設定 - */ "mfmAndAnimatedImagesSetting": string; - /** - * これらの設定は後から変更できます。 - */ "theseSettingsCanEditLater": string; - /** - * この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。 - */ "youCanEditMoreSettingsInSettingsPageLater": string; - /** - * タイムラインを構築するため、気になるユーザーをフォローしてみましょう。 - */ "followUsers": string; - /** - * プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。 - */ - "pushNotificationDescription": ParameterizedString<"name">; - /** - * 初期設定が完了しました! - */ + "pushNotificationDescription": string; "initialAccountSettingCompleted": string; - /** - * {name}をお楽しみください! - */ - "haveFun": ParameterizedString<"name">; - /** - * このまま{name}(CherryPick)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。 - */ - "youCanContinueTutorial": ParameterizedString<"name">; - /** - * チュートリアルを開始 - */ + "haveFun": string; + "youCanContinueTutorial": string; "startTutorial": string; - /** - * 初期設定をスキップしますか? - */ "skipAreYouSure": string; - /** - * 今すぐ初期設定を中断しても、[もっと! - ヘルプ - 初期設定のリプレイ]から再開することができます。 - */ "skipAreYouSureDescription": string; - /** - * 初期設定をあとでやり直しますか? - */ "laterAreYouSure": string; }; "_initialTutorial": { - /** - * チュートリアルを見る - */ "launchTutorial": string; - /** - * チュートリアル - */ "title": string; - /** - * よくできました - */ "wellDone": string; - /** - * チュートリアルを終了しますか? - */ "skipAreYouSure": string; "_landing": { - /** - * チュートリアルへようこそ - */ "title": string; - /** - * ここでは、CherryPickの基本的な使い方や機能を確認できます。 - */ "description": string; }; "_note": { - /** - * ノートって何? - */ "title": string; - /** - * CherryPickでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。 - */ "description": string; - /** - * 返信することができます。返信に対しての返信も可能で、スレッドのように会話を続けることもできます。 - */ "reply": string; - /** - * そのノートを自分のタイムラインに流して共有することができます。テキストを追加して引用することも可能です。 - */ "renote": string; - /** - * ハートリアクションを送ることができます。「いいね!」を素早く残したいときに便利です。 - */ "like": string; - /** - * リアクションをつけることができます。詳しくは次のページで解説します。 - */ "reaction": string; - /** - * 引用を付けることができます。何らかの内容に基づき意見を付け加えたいときに便利です。 - */ "quote": string; - /** - * ノートの詳細を表示したり、リンクをコピーしたりなどの様々な操作が行えます。 - */ "menu": string; }; "_reaction": { - /** - * リアクションって何? - */ "title": string; - /** - * ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。 - */ "description": string; - /** - * リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください! - */ "letsTryReacting": string; - /** - * リアクションをつけると先に進めるようになります。 - */ "reactToContinue": string; - /** - * あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。 - */ "reactNotification": string; - /** - * 「ー」ボタンを押すとリアクションを取り消すことができます。 - */ "reactDone": string; }; "_timeline": { - /** - * タイムラインのしくみ - */ "title": string; - /** - * CherryPickには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。 - */ "description1": string; - /** - * あなたがフォローしているアカウントの投稿を見られます。 - */ "home": string; - /** - * このサーバーにいるユーザー全員の投稿を見られます。 - */ "local": string; - /** - * ホームタイムラインとローカルタイムラインの投稿が両方表示されます。 - */ "social": string; - /** - * 接続している他のすべてのサーバーからの投稿を見られます。 - */ "global": string; - /** - * それぞれのタイムラインは、画面上部でいつでも切り替えられます。 - */ "description2": string; - /** - * その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。 - */ - "description3": ParameterizedString<"link">; + "description3": string; }; "_postNote": { - /** - * ノートの投稿設定 - */ "title": string; - /** - * CherryPickにノートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。 - */ "description1": string; "_visibility": { - /** - * ノートを表示できる相手を制限できます。 - */ "description": string; - /** - * すべてのユーザーに公開。 - */ "public": string; - /** - * ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。 - */ "home": string; - /** - * フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。 - */ "followers": string; - /** - * 指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。 - */ "direct": string; - /** - * 機密情報は送信する際は注意してください。 - */ "doNotSendConfidencialOnDirect1": string; - /** - * 送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。 - */ "doNotSendConfidencialOnDirect2": string; - /** - * 他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。 - */ "localOnly": string; }; "_cw": { - /** - * 内容を隠す(CW) - */ "title": string; - /** - * 本文のかわりに「注釈」に書いた内容が表示されます。「もっと見る」を押すと本文が表示されます。 - */ "description": string; "_exampleNote": { - /** - * 飯テロ注意 - */ "cw": string; - /** - * チョコのかかったドーナツを食べました🍩😋 - */ "note": string; }; - /** - * サーバーのガイドラインにより必要とされるノートに指定したり、ネタバレ投稿やセンシティブな文章を自主規制したりするときに使います。 - */ "useCases": string; }; }; "_howToMakeAttachmentsSensitive": { - /** - * 添付ファイルをセンシティブにするには? - */ "title": string; - /** - * サーバーのガイドラインにより必要とされる際や、そのまま見れる状態にしておくべきではない添付ファイルには、「センシティブ」設定を付けます。 - */ "description": string; - /** - * 試しに、このフォームに添付された画像をセンシティブにしてみてください! - */ "tryThisFile": string; "_exampleNote": { - /** - * 納豆のフタ開けるのミスったわね… - */ "note": string; }; - /** - * 添付ファイルをセンシティブにする際は、そのファイルをクリックしてメニューを開き、「センシティブとして設定」をクリックします。 - */ "method": string; - /** - * ファイルを添付する際は、サーバーのガイドラインに従ってセンシティブを適切に設定してください。 - */ "sensitiveSucceeded": string; - /** - * 画像をセンシティブに設定すると先に進めるようになります。 - */ "doItToContinue": string; }; "_done": { - /** - * チュートリアルは終了です🎉 - */ "title": string; - /** - * ここで紹介した機能はほんの一部にすぎません。CherryPickの使い方をより詳しく知るには、{link}をご覧ください。 - */ - "description": ParameterizedString<"link">; + "description": string; }; }; "_timelineDescription": { - /** - * ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。 - */ "home": string; - /** - * ローカルタイムラインでは、このサーバーにいるユーザー全員の投稿を見られます。 - */ "local": string; - /** - * ソーシャルタイムラインには、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。 - */ "social": string; - /** - * グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。 - */ "global": string; }; "_serverRules": { - /** - * 新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。 - */ "description": string; }; "_event": { - /** - * 開始日時 - */ "startDateTime": string; - /** - * 終了日時 - */ "endDateTime": string; - /** - * 開始日 - */ "startDate": string; - /** - * 終了日 - */ "endDate": string; - /** - * 開始時刻 - */ "startTime": string; - /** - * 終了時刻 - */ "endTime": string; - /** - * 属性 - */ "detailName": string; - /** - * 値 - */ "detailValue": string; - /** - * 所在地 - */ "location": string; - /** - * ドアタイム - */ "doorTime": string; - /** - * 主催者 - */ "organizer": string; - /** - * 主催者リンク - */ "organizerLink": string; - /** - * オーディエンス - */ "audience": string; - /** - * 言語 - */ "language": string; - /** - * 年齢層 - */ "ageRange": string; - /** - * チケット - */ "ticketsUrl": string; - /** - * 無料 - */ "isFree": string; - /** - * 価格 - */ "price": string; - /** - * 予約可能 - */ "availability": string; - /** - * から - */ "from": string; - /** - * まで - */ "until": string; - /** - * 予約開始 - */ "availabilityStart": string; - /** - * 予約終了 - */ "availabilityEnd": string; - /** - * キーワード - */ "keywords": string; - /** - * 出演者 - */ "performers": string; }; "_serverSettings": { - /** - * アイコン画像のURL - */ "iconUrl": string; - /** - * {host}がアプリとして表示される際のアイコンを指定します。 - */ - "appIconDescription": ParameterizedString<"host">; - /** - * 例: PWAや、スマートフォンのホーム画面にブックマークとして追加された時など - */ + "appIconDescription": string; "appIconUsageExample": string; - /** - * 円形もしくは角丸にクロップされる場合があるため、塗り潰された余白のある背景を持つことが推奨されます。 - */ "appIconStyleRecommendation": string; - /** - * 解像度は必ず{resolution}である必要があります。 - */ - "appIconResolutionMustBe": ParameterizedString<"resolution">; - /** - * manifest.jsonのオーバーライド - */ + "appIconResolutionMustBe": string; "manifestJsonOverride": string; - /** - * 略称 - */ "shortName": string; - /** - * サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。 - */ "shortNameDescription": string; - /** - * 有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。 - */ "fanoutTimelineDescription": string; - /** - * データベースへのフォールバック - */ "fanoutTimelineDbFallback": string; - /** - * 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。 - */ "fanoutTimelineDbFallbackDescription": string; - /** - * 問い合わせ先URL - */ - "inquiryUrl": string; - /** - * サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。 - */ - "inquiryUrlDescription": string; }; "_accountMigration": { - /** - * 別のアカウントからこのアカウントに移行 - */ "moveFrom": string; - /** - * 別のアカウントへエイリアスを作成 - */ "moveFromSub": string; - /** - * 移行元のアカウント #{n} - */ - "moveFromLabel": ParameterizedString<"n">; - /** - * 別のアカウントからこのアカウントに移行したい場合、ここでエイリアスを作成しておく必要があります。 - * 移行元のアカウントをこのように入力してください: @username@server.example.com - * 削除するには、入力欄を空にして保存します(非推奨)。 - */ + "moveFromLabel": string; "moveFromDescription": string; - /** - * このアカウントを新しいアカウントへ移行 - */ "moveTo": string; - /** - * 移行先のアカウント: - */ "moveToLabel": string; - /** - * アカウントを移行すると、取り消すことはできません。 - */ "moveCannotBeUndone": string; - /** - * 新しいアカウントへ移行します。 - *  ・フォロワーが新しいアカウントを自動でフォローします - *  ・このアカウントからのフォローは全て解除されます - *  ・このアカウントではノートの作成などができなくなります - * - * フォロワーの移行は自動ですが、フォローの移行は手動で行う必要があります。移行前にこのアカウントでフォローエクスポートし、移行後すぐに移行先アカウントでインポートを行なってください。 - * リスト・ミュート・ブロックについても同様ですので、手動で移行する必要があります。 - * - * (この説明はこのサーバー(CherryPick v13.12.0以降)の仕様です。Mastodonなどの他のActivityPubソフトウェアでは挙動が異なる場合があります。) - */ "moveAccountDescription": string; - /** - * アカウントの移行には、まずは移行先のアカウントでこのアカウントに対しエイリアスを作成します。 - * エイリアス作成後、移行先のアカウントを次のように入力してください: @username@server.example.com - */ "moveAccountHowTo": string; - /** - * 移行する - */ "startMigration": string; - /** - * 本当にこのアカウントを {account} に移行しますか?一度移行すると取り消せず、二度とこのアカウントを元の状態で使用できなくなります。 - */ - "migrationConfirm": ParameterizedString<"account">; - /** - * - * アカウントは移行されています。 - * 移行を取り消すことはできません。 - */ + "migrationConfirm": string; "movedAndCannotBeUndone": string; - /** - * このアカウントからのフォロー解除は移行操作から24時間後に実行されます。 - * このアカウントのフォロー・フォロワー数は0になっています。フォロワーの解除はされないため、あなたのフォロワーはこのアカウントのフォロワー向け投稿を引き続き閲覧できます。 - */ "postMigrationNote": string; - /** - * 移行先のアカウント: - */ "movedTo": string; }; "_achievements": { - /** - * 獲得日時 - */ "earnedAt": string; "_types": { "_notes1": { - /** - * just setting up my crpk - */ "title": string; - /** - * 初めてノートを投稿した - */ "description": string; - /** - * 良いCherryPickライフを! - */ "flavor": string; }; "_notes10": { - /** - * いくつかのノート - */ "title": string; - /** - * ノートを10回投稿した - */ "description": string; }; "_notes100": { - /** - * たくさんのノート - */ "title": string; - /** - * ノートを100回投稿した - */ "description": string; }; "_notes500": { - /** - * ノートまみれ - */ "title": string; - /** - * ノートを500回投稿した - */ "description": string; }; "_notes1000": { - /** - * ノートの山 - */ "title": string; - /** - * ノートを1,000回投稿した - */ "description": string; }; "_notes5000": { - /** - * 湧き出るノート - */ "title": string; - /** - * ノートを5,000回投稿した - */ "description": string; }; "_notes10000": { - /** - * スーパーノート - */ "title": string; - /** - * ノートを10,000回投稿した - */ "description": string; }; "_notes20000": { - /** - * ニードモアノート - */ "title": string; - /** - * ノートを20,000回投稿した - */ "description": string; }; "_notes30000": { - /** - * ノートノートノート - */ "title": string; - /** - * ノートを30,000回投稿した - */ "description": string; }; "_notes40000": { - /** - * ノート工場 - */ "title": string; - /** - * ノートを40,000回投稿した - */ "description": string; }; "_notes50000": { - /** - * ノートの惑星 - */ "title": string; - /** - * ノートを50,000回投稿した - */ "description": string; }; "_notes60000": { - /** - * ノートクエーサー - */ "title": string; - /** - * ノートを60,000回投稿した - */ "description": string; }; "_notes70000": { - /** - * ブラックノートホール - */ "title": string; - /** - * ノートを70,000回投稿した - */ "description": string; }; "_notes80000": { - /** - * ノートギャラクシー - */ "title": string; - /** - * ノートを80,000回投稿した - */ "description": string; }; "_notes90000": { - /** - * ノートバース - */ "title": string; - /** - * ノートを90,000回投稿した - */ "description": string; }; "_notes100000": { - /** - * ALL YOUR NOTE ARE BELONG TO US - */ "title": string; - /** - * ノートを100,000回投稿した - */ "description": string; - /** - * そんなに書くことある? - */ "flavor": string; }; "_login3": { - /** - * ビギナーⅠ - */ "title": string; - /** - * 通算ログイン日数が3日 - */ "description": string; - /** - * 今日からね僕は チェリーピキストってことで - */ "flavor": string; }; "_login7": { - /** - * ビギナーⅡ - */ "title": string; - /** - * 通算ログイン日数が7日 - */ "description": string; - /** - * 慣れてきましたか? - */ "flavor": string; }; "_login15": { - /** - * ビギナーⅢ - */ "title": string; - /** - * 通算ログイン日数が15日 - */ "description": string; }; "_login30": { - /** - * ミスキストⅠ - */ "title": string; - /** - * 通算ログイン日数が30日 - */ "description": string; }; "_login60": { - /** - * ミスキストⅡ - */ "title": string; - /** - * 通算ログイン日数が60日 - */ "description": string; }; "_login100": { - /** - * ミスキストⅢ - */ "title": string; - /** - * 通算ログイン日数が100日 - */ "description": string; - /** - * そのユーザー、ミスキストにつき - */ "flavor": string; }; "_login200": { - /** - * 常連Ⅰ - */ "title": string; - /** - * 通算ログイン日数が200日 - */ "description": string; }; "_login300": { - /** - * 常連Ⅱ - */ "title": string; - /** - * 通算ログイン日数が300日 - */ "description": string; }; "_login400": { - /** - * 常連Ⅲ - */ "title": string; - /** - * 通算ログイン日数が400日 - */ "description": string; }; "_login500": { - /** - * ベテランⅠ - */ "title": string; - /** - * 通算ログイン日数が500日 - */ "description": string; - /** - * 諸君、私はノートが好きだ - */ "flavor": string; }; "_login600": { - /** - * ベテランⅡ - */ "title": string; - /** - * 通算ログイン日数が600日 - */ "description": string; }; "_login700": { - /** - * ベテランⅢ - */ "title": string; - /** - * 通算ログイン日数が700日 - */ "description": string; }; "_login800": { - /** - * ノートマスターⅠ - */ "title": string; - /** - * 通算ログイン日数が800日 - */ "description": string; }; "_login900": { - /** - * ノートマスターⅡ - */ "title": string; - /** - * 通算ログイン日数が900日 - */ "description": string; }; "_login1000": { - /** - * ノートマスターⅢ - */ "title": string; - /** - * 通算ログイン日数が1,000日 - */ "description": string; - /** - * CherryPickを使ってくれてありがとう! - */ "flavor": string; }; "_noteClipped1": { - /** - * クリップせずにはいられないな - */ "title": string; - /** - * 初めてノートをクリップした - */ "description": string; }; "_noteFavorited1": { - /** - * 星をみるひと - */ "title": string; - /** - * 初めてノートをお気に入りに登録した - */ "description": string; }; "_myNoteFavorited1": { - /** - * 星が欲しい - */ "title": string; - /** - * 自分のノートが他の人からお気に入りに登録された - */ "description": string; }; "_profileFilled": { - /** - * 準備万端 - */ "title": string; - /** - * プロフィール設定を行った - */ "description": string; }; "_markedAsCat": { - /** - * 吾輩は猫である - */ "title": string; - /** - * アカウントをCatとして設定した - */ "description": string; - /** - * 名前はまだない。 - */ "flavor": string; }; "_following1": { - /** - * はじめてのフォロー - */ "title": string; - /** - * 初めてフォローした - */ "description": string; }; "_following10": { - /** - * ついてく、ついてく - */ "title": string; - /** - * フォローが10人を超した - */ "description": string; }; "_following50": { - /** - * 友達たくさん - */ "title": string; - /** - * フォローが50人を超した - */ "description": string; }; "_following100": { - /** - * 友達100人 - */ "title": string; - /** - * フォローが100人を超した - */ "description": string; }; "_following300": { - /** - * 友達過多 - */ "title": string; - /** - * フォローが300人を超した - */ "description": string; }; "_followers1": { - /** - * はじめてのフォロワー - */ "title": string; - /** - * 初めてフォローされた - */ "description": string; }; "_followers10": { - /** - * フォローミー! - */ "title": string; - /** - * フォロワーが10人を超した - */ "description": string; }; "_followers50": { - /** - * ぞろぞろ - */ "title": string; - /** - * フォロワーが50人を超した - */ "description": string; }; "_followers100": { - /** - * 人気者 - */ "title": string; - /** - * フォロワーが100人を超した - */ "description": string; }; "_followers300": { - /** - * 一列でお並びください - */ "title": string; - /** - * フォロワーが300人を超した - */ "description": string; }; "_followers500": { - /** - * 基地局 - */ "title": string; - /** - * フォロワーが500人を超した - */ "description": string; }; "_followers1000": { - /** - * インフルエンサー - */ "title": string; - /** - * フォロワーが1,000人を超した - */ "description": string; }; "_collectAchievements30": { - /** - * 実績コレクター - */ "title": string; - /** - * 実績を30個以上獲得した - */ "description": string; }; "_viewAchievements3min": { - /** - * 実績好き - */ "title": string; - /** - * 実績一覧を3分以上眺め続けた - */ "description": string; }; "_iLoveCherryPick": { - /** - * I Love CherryPick - */ "title": string; - /** - * "I ❤ #CherryPick"を投稿した - */ "description": string; - /** - * CherryPickを使ってくださりありがとうございます! by 開発チーム - */ "flavor": string; }; "_foundTreasure": { - /** - * 宝探し - */ "title": string; - /** - * 隠されたお宝を発見した - */ "description": string; }; "_client30min": { - /** - * ひとやすみ - */ "title": string; - /** - * クライアントを起動してから30分以上経過した - */ "description": string; }; "_client60min": { - /** - * CherryPickの見すぎ - */ "title": string; - /** - * クライアントを起動してから60分以上経過した - */ "description": string; }; "_noteDeletedWithin1min": { - /** - * いまのなし - */ "title": string; - /** - * 投稿してから1分以内にその投稿を削除した - */ "description": string; }; "_postedAtLateNight": { - /** - * 夜行性 - */ "title": string; - /** - * 深夜にノートを投稿した - */ "description": string; - /** - * そろそろ寝よう。 - */ "flavor": string; }; "_postedAt0min0sec": { - /** - * 時報 - */ "title": string; - /** - * 0分0秒にノートを投稿した - */ "description": string; - /** - * ポッ ポッ ポッ ピーン - */ "flavor": string; }; "_selfQuote": { - /** - * 自己言及 - */ "title": string; - /** - * 自分のノートを引用した - */ "description": string; }; "_htl20npm": { - /** - * 流れるTL - */ "title": string; - /** - * ホームタイムラインの流速が20npmを越す - */ "description": string; }; "_viewInstanceChart": { - /** - * アナリスト - */ "title": string; - /** - * サーバーのチャートを表示した - */ "description": string; }; "_outputHelloWorldOnScratchpad": { - /** - * Hello, world! - */ "title": string; - /** - * スクラッチパッドで hello world を出力した - */ "description": string; }; "_open3windows": { - /** - * マルチウィンドウ - */ "title": string; - /** - * ウィンドウを3つ以上開いた状態にした - */ "description": string; }; "_driveFolderCircularReference": { - /** - * 循環参照 - */ "title": string; - /** - * ドライブのフォルダを再帰的な入れ子にしようとした - */ "description": string; }; "_reactWithoutRead": { - /** - * ちゃんと読んだ? - */ "title": string; - /** - * 100文字以上のテキストを含むノートに投稿されてから3秒以内にリアクションした - */ "description": string; }; "_clickedClickHere": { - /** - * ここをクリック - */ "title": string; - /** - * ここをクリックした - */ "description": string; }; "_justPlainLucky": { - /** - * 単なるラッキー - */ "title": string; - /** - * 10秒ごとに0.005%の確率で獲得 - */ "description": string; }; "_setNameToSyuilo": { - /** - * 神様コンプレックス - */ "title": string; - /** - * 名前を syuilo に設定した - */ "description": string; }; "_setNameToNoriDev": { - /** - * 神様コンプレックス(CherryPick) - */ "title": string; - /** - * 名前を noridev に設定した - */ "description": string; }; - "_setNameToYojo": { - /** - * ロリータコンプレックス - */ - "title": string; - /** - * 名前を 幼女 に設定した - */ - "description": string; - /** - * これであなたもロリコン - */ - "flavor": string; - }; "_passedSinceAccountCreated1": { - /** - * 一周年 - */ "title": string; - /** - * アカウント作成から1年経過した - */ "description": string; }; "_passedSinceAccountCreated2": { - /** - * 二周年 - */ "title": string; - /** - * アカウント作成から2年経過した - */ "description": string; }; "_passedSinceAccountCreated3": { - /** - * 三周年 - */ "title": string; - /** - * アカウント作成から3年経過した - */ "description": string; }; "_loggedInOnBirthday": { - /** - * ハッピーバースデー - */ "title": string; - /** - * 誕生日にログインした - */ "description": string; }; "_loggedInOnNewYearsDay": { - /** - * あけましておめでとうございます - */ "title": string; - /** - * 元日にログインした - */ "description": string; - /** - * 今年も弊サーバーをよろしくお願いします - */ "flavor": string; }; "_cookieClicked": { - /** - * クッキーをクリックするゲーム - */ "title": string; - /** - * クッキーをクリックした - */ "description": string; - /** - * ソフト間違ってない? - */ "flavor": string; }; "_brainDiver": { - /** - * Brain Diver - */ "title": string; - /** - * Brain Diverへのリンクを投稿した - */ "description": string; - /** - * Misskey-Misskey La-Tu-Ma - */ "flavor": string; }; "_smashTestNotificationButton": { - /** - * テスト過剰 - */ "title": string; - /** - * 通知のテストをごく短時間のうちに連続して行った - */ "description": string; }; "_tutorialCompleted": { - /** - * CherryPick初心者講座 修了証 - */ - "title": string; - /** - * チュートリアルを完了した - */ - "description": string; - }; - "_bubbleGameExplodingHead": { - /** - * 🤯 - */ - "title": string; - /** - * バブルゲームで最も大きいモノを出した - */ - "description": string; - }; - "_bubbleGameDoubleExplodingHead": { - /** - * ダブル🤯 - */ - "title": string; - /** - * バブルゲームで最も大きいモノを2つ同時に出した - */ - "description": string; - /** - * これくらいの おべんとばこに 🤯 🤯 ちょっとつめて - */ - "flavor": string; - }; - "_ohayoujo1": { - /** - * 今日も一日! - */ - "title": string; - /** - * おはようようじょー!と投稿した - */ - "description": string; - /** - * おはようじょー! - */ - "flavor": string; - }; - "_ohayoujo7": { - /** - * 元気いっぱい! - */ "title": string; - /** - * おはようようじょー!と7日投稿した - */ "description": string; - /** - * おはようじょー! - */ - "flavor": string; - }; - "_ohayoujo30": { - /** - * 笑顔満点 - */ - "title": string; - /** - * おはようようじょー!と30日投稿した - */ - "description": string; - /** - * おはようじょー! - */ - "flavor": string; - }; - "_ohayoujo365": { - /** - * 皆勤賞 - */ - "title": string; - /** - * おはようようじょー!と365日投稿した - */ - "description": string; - /** - * おはようじょー! - */ - "flavor": string; }; }; }; "_role": { - /** - * ロールの作成 - */ "new": string; - /** - * ロールの編集 - */ "edit": string; - /** - * ロール名 - */ "name": string; - /** - * ロールの説明 - */ "description": string; - /** - * ロールの権限 - */ "permission": string; - /** - * モデレーターは基本的なモデレーションに関する操作を行えます。 - * 管理者はサーバーの全ての設定を変更できます。 - */ "descriptionOfPermission": string; - /** - * アサイン - */ "assignTarget": string; - /** - * マニュアルは誰がこのロールに含まれるかを手動で管理します。 - * コンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。 - */ "descriptionOfAssignTarget": string; - /** - * マニュアル - */ "manual": string; - /** - * マニュアルロール - */ "manualRoles": string; - /** - * コンディショナル - */ "conditional": string; - /** - * コンディショナルロール - */ "conditionalRoles": string; - /** - * 条件 - */ "condition": string; - /** - * これはコンディショナルロールです。 - */ "isConditionalRole": string; - /** - * 公開ロール - */ "isPublic": string; - /** - * ユーザーのプロフィールでこのロールが表示されます。 - */ "descriptionOfIsPublic": string; - /** - * オプション - */ "options": string; - /** - * ポリシー - */ "policies": string; - /** - * ベースロール - */ "baseRole": string; - /** - * ベースロールの値を使用 - */ "useBaseValue": string; - /** - * アサインするロールを選択 - */ "chooseRoleToAssign": string; - /** - * アイコン画像のURL - */ "iconUrl": string; - /** - * バッジとして表示 - */ "asBadge": string; - /** - * オンにすると、ユーザー名の横にロールのアイコンが表示されます。 - */ "descriptionOfAsBadge": string; - /** - * ユーザーを見つけやすくする - */ "isExplorable": string; - /** - * オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。 - */ "descriptionOfIsExplorable": string; - /** - * 表示順 - */ "displayOrder": string; - /** - * 数値が大きいほどUI上で先頭に表示されます。 - */ "descriptionOfDisplayOrder": string; - /** - * モデレーターのメンバー編集を許可 - */ "canEditMembersByModerator": string; - /** - * オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。 - */ "descriptionOfCanEditMembersByModerator": string; - /** - * 優先度 - */ "priority": string; "_priority": { - /** - * 低 - */ "low": string; - /** - * 中 - */ "middle": string; - /** - * 高 - */ "high": string; }; "_options": { - /** - * グローバルタイムラインの閲覧 - */ "gtlAvailable": string; - /** - * ローカルタイムラインの閲覧 - */ "ltlAvailable": string; - /** - * パブリック投稿の許可 - */ "canPublicNote": string; - /** - * ノートの編集 - */ "canEditNote": string; - /** - * ノート内の最大メンション数 - */ - "mentionMax": string; - /** - * サーバー招待コードの発行 - */ "canInvite": string; - /** - * 招待コードの作成可能数 - */ "inviteLimit": string; - /** - * 招待コードの発行間隔 - */ "inviteLimitCycle": string; - /** - * 招待コードの有効期限 - */ "inviteExpirationTime": string; - /** - * カスタム絵文字の管理 - */ "canManageCustomEmojis": string; - /** - * アバターデコレーションの管理 - */ "canManageAvatarDecorations": string; - /** - * ドライブ容量 - */ "driveCapacity": string; - /** - * ファイルサイズ上限 - */ - "fileSizeLimit": string; - /** - * ファイルにNSFWを常に付与 - */ "alwaysMarkNsfw": string; - /** - * アイコンとバナーの更新を許可 - */ - "canUpdateBioMedia": string; - /** - * ノートのピン留めの最大数 - */ "pinMax": string; - /** - * アンテナの作成可能数 - */ "antennaMax": string; - /** - * ワードミュートの最大文字数 - */ "wordMuteMax": string; - /** - * Webhookの作成可能数 - */ "webhookMax": string; - /** - * クリップの作成可能数 - */ "clipMax": string; - /** - * クリップ内のノートの最大数 - */ "noteEachClipsMax": string; - /** - * ユーザーリストの作成可能数 - */ "userListMax": string; - /** - * ユーザーリスト内のユーザーの最大数 - */ "userEachUserListsMax": string; - /** - * レートリミット - */ "rateLimitFactor": string; - /** - * 小さいほど制限が緩和され、大きいほど制限が強化されます。 - */ "descriptionOfRateLimitFactor": string; - /** - * 広告の非表示 - */ "canHideAds": string; - /** - * ノート検索の利用 - */ "canSearchNotes": string; - /** - * 高度な検索の利用 - */ - "canAdvancedSearchNotes": string; - /** - * 翻訳機能の利用 - */ "canUseTranslator": string; - /** - * アイコンデコレーションの最大取付個数 - */ "avatarDecorationLimit": string; }; "_condition": { - /** - * マニュアルロールにアサイン済み - */ - "roleAssignedTo": string; - /** - * ローカルユーザー - */ "isLocal": string; - /** - * リモートユーザー - */ "isRemote": string; - /** - * 猫ユーザー - */ - "isCat": string; - /** - * botユーザー - */ - "isBot": string; - /** - * サスペンド済みユーザー - */ - "isSuspended": string; - /** - * 鍵アカウントユーザー - */ - "isLocked": string; - /** - * 「アカウントを見つけやすくする」が有効なユーザー - */ - "isExplorable": string; - /** - * アカウント作成から~以内 - */ "createdLessThan": string; - /** - * アカウント作成から~経過 - */ "createdMoreThan": string; - /** - * フォロワー数が~以下 - */ "followersLessThanOrEq": string; - /** - * フォロワー数が~以上 - */ "followersMoreThanOrEq": string; - /** - * フォロー数が~以下 - */ "followingLessThanOrEq": string; - /** - * フォロー数が~以上 - */ "followingMoreThanOrEq": string; - /** - * 投稿数が~以下 - */ "notesLessThanOrEq": string; - /** - * 投稿数が~以上 - */ "notesMoreThanOrEq": string; - /** - * ~かつ~ - */ "and": string; - /** - * ~または~ - */ "or": string; - /** - * ~ではない - */ "not": string; }; }; "_newNoteReceivedNotificationBehavior": { - /** - * デフォルト - */ "default": string; - /** - * ノート数表示 - */ "count": string; - /** - * 表示なし - */ "none": string; }; "_sensitiveMediaDetection": { - /** - * 機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。 - */ "description": string; - /** - * 検出感度 - */ "sensitivity": string; - /** - * 感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。 - */ "sensitivityDescription": string; - /** - * センシティブフラグを設定する - */ "setSensitiveFlagAutomatically": string; - /** - * この設定をオフにしても内部的に判定結果は保持されます。 - */ "setSensitiveFlagAutomaticallyDescription": string; - /** - * 動画の解析を有効化 - */ "analyzeVideos": string; - /** - * 静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。 - */ "analyzeVideosDescription": string; }; "_emailUnavailable": { - /** - * 既に使用されています - */ "used": string; - /** - * 形式が正しくありません - */ "format": string; - /** - * 恒久的に使用可能なアドレスではありません - */ "disposable": string; - /** - * 正しいメールサーバーではありません - */ "mx": string; - /** - * メールサーバーが応答しません - */ "smtp": string; - /** - * このメールアドレスでは登録できません - */ "banned": string; }; "_ffVisibility": { - /** - * 公開 - */ "public": string; - /** - * フォロワーだけに公開 - */ "followers": string; - /** - * 非公開 - */ "private": string; }; "_signup": { - /** - * ほとんど完了です - */ "almostThere": string; - /** - * あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。 - */ "emailAddressInfo": string; - /** - * 入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。 - * もしメールが来なかったらスパムメールボックスを確認してください。 - * メールに記載されているリンクの有効期限は30分です。 - */ - "emailSent": ParameterizedString<"email">; + "emailSent": string; }; "_accountDelete": { - /** - * アカウントの削除 - */ "accountDelete": string; - /** - * アカウントの削除は負荷のかかる処理であるため、作成したコンテンツの数やアップロードしたファイルの数が多いと完了までに時間がかかることがあります。 - */ "mayTakeTime": string; - /** - * アカウントの削除が完了する際は、登録してあったメールアドレス宛に通知を送信します。 - */ "sendEmail": string; - /** - * アカウント削除をリクエスト - */ "requestAccountDelete": string; - /** - * 削除処理が開始されました。 - */ "started": string; - /** - * 削除が進行中 - */ "inProgress": string; }; "_ad": { - /** - * 戻る - */ "back": string; - /** - * この広告の表示頻度を下げる - */ "reduceFrequencyOfThisAd": string; - /** - * 表示しない - */ "hide": string; - /** - * 曜日はサーバーのタイムゾーンを元に指定されます。 - */ "timezoneinfo": string; - /** - * 広告配信設定 - */ "adsSettings": string; - /** - * リアルタイム更新中に広告を配信する間隔(ノートの個数) - */ "notesPerOneAd": string; - /** - * 0でリアルタイム更新時の広告配信を無効 - */ "setZeroToDisable": string; - /** - * 広告の配信間隔が極めて短いため、ユーザー体験が著しく損われる可能性があります。 - */ "adsTooClose": string; }; "_forgotPassword": { - /** - * アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。 - */ "enterEmail": string; - /** - * メールアドレスを登録していない場合は、管理者までお問い合わせください。 - */ "ifNoEmail": string; - /** - * このサーバーではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。 - */ "contactAdmin": string; }; "_gallery": { - /** - * 自分の投稿 - */ "my": string; - /** - * いいねした投稿 - */ "liked": string; - /** - * いいね! - */ "like": string; - /** - * いいね解除 - */ "unlike": string; }; "_email": { "_follow": { - /** - * フォローされました - */ "title": string; }; "_receiveFollowRequest": { - /** - * フォローリクエストを受け取りました - */ "title": string; }; }; "_plugin": { - /** - * プラグインのインストール - */ "install": string; - /** - * 信頼できないプラグインはインストールしないでください。 - */ "installWarn": string; - /** - * プラグインの管理 - */ "manage": string; - /** - * ソースを表示 - */ "viewSource": string; - /** - * ログを表示 - */ - "viewLog": string; }; "_preferencesBackups": { - /** - * 作成したバックアップ - */ "list": string; - /** - * 新規保存 - */ "saveNew": string; - /** - * ファイルを読み込み - */ "loadFile": string; - /** - * このデバイスに適用 - */ "apply": string; - /** - * 上書き保存 - */ "save": string; - /** - * バックアップ名を入力 - */ "inputName": string; - /** - * 保存できません - */ "cannotSave": string; - /** - * バックアップ名「{name}」は既に存在します。違う名前を指定してください。 - */ - "nameAlreadyExists": ParameterizedString<"name">; - /** - * バックアップ「{name}」を現在のデバイスに適用しますか?現在のデバイス設定は失われます。 - */ - "applyConfirm": ParameterizedString<"name">; - /** - * {name}に上書き保存しますか? - */ - "saveConfirm": ParameterizedString<"name">; - /** - * {name}を削除しますか? - */ - "deleteConfirm": ParameterizedString<"name">; - /** - * 「{old}」を「{new}」に変更しますか? - */ - "renameConfirm": ParameterizedString<"old" | "new">; - /** - * バックアップはありません。「新規保存」で現在のクライアント設定をサーバーに保存できます。 - */ + "nameAlreadyExists": string; + "applyConfirm": string; + "saveConfirm": string; + "deleteConfirm": string; + "renameConfirm": string; "noBackups": string; - /** - * 作成日時: {date} {time} - */ - "createdAt": ParameterizedString<"date" | "time">; - /** - * 更新日時: {date} {time} - */ - "updatedAt": ParameterizedString<"date" | "time">; - /** - * 読み込みできません - */ + "createdAt": string; + "updatedAt": string; "cannotLoad": string; - /** - * ファイル形式が違います。 - */ "invalidFile": string; }; "_registry": { - /** - * スコープ - */ "scope": string; - /** - * キー - */ "key": string; - /** - * キー - */ "keys": string; - /** - * ドメイン - */ "domain": string; - /** - * キーを作成 - */ "createKey": string; }; "_aboutMisskey": { - /** - * CherryPickは、Misskeyをベースに2021年から開発中のカスタマイズオープンソースのソフトウェアです。 - */ "about": string; - /** - * コントリビューター - */ "contributors": string; - /** - * 全てのコントリビューター - */ "allContributors": string; - /** - * ソースコード - */ "source": string; - /** - * オリジナル - */ - "original": string; - /** - * {name}はオリジナルのCherryPickを改変したバージョンを使用しています。 - */ - "thisIsModifiedVersion": ParameterizedString<"name">; - /** - * Misskeyを翻訳 - */ "translation": string; - /** - * Misskeyに寄付 - */ "donate": string; - /** - * 他にも多くの方が支援してくれています。ありがとうございます🥰 - */ "morePatrons": string; - /** - * 支援者 - */ "patrons": string; - /** - * プロジェクトメンバー - */ "projectMembers": string; "_kokonect": { - /** - * サーバ状態 - */ "serverStatus": string; - /** - * ココネクトに寄付 - */ "donate": string; }; "_cherrypick": { - /** - * CherryPickを翻訳 - */ "translation": string; - /** - * CherryPickに寄付 - */ "donate": string; - /** - * リレーサーバー - */ "relayServer": string; - /** - * コミュニティー - */ "community": string; }; }; "_displayOfSensitiveMedia": { - /** - * センシティブ設定されたメディアを隠す - */ "respect": string; - /** - * センシティブ設定されたメディアを隠さない - */ "ignore": string; - /** - * 常にメディアを隠す - */ "force": string; }; "_mfm": { - /** - * MFMチートシート - */ "cheatSheet": string; - /** - * MFMは、Misskey内の様々な場所で使用できる専用のマークアップ言語です。ここでは、MFMで使用可能な構文一覧が確認できます。 - */ "intro": string; - /** - * CherryPickでFediverseの世界が広がります - */ "dummy": string; - /** - * メンション - */ "mention": string; - /** - * アットマーク + ユーザー名で、特定のユーザーを示すことができます。 - */ "mentionDescription": string; - /** - * ハッシュタグ - */ "hashtag": string; - /** - * ナンバーサイン + タグで、ハッシュタグを示すことができます。 - */ "hashtagDescription": string; - /** - * URL - */ "url": string; - /** - * URLを示すことができます。 - */ "urlDescription": string; - /** - * リンク - */ "link": string; - /** - * 文章の特定の範囲を、URLに紐づけることができます。 - */ "linkDescription": string; - /** - * 太字 - */ "bold": string; - /** - * 文字を太く表示して強調することができます。 - */ "boldDescription": string; - /** - * 目立たなく - */ "small": string; - /** - * 内容を小さく・薄く表示させることができます。 - */ "smallDescription": string; - /** - * 中央寄せ - */ "center": string; - /** - * 内容を中央寄せで表示させることができます。 - */ "centerDescription": string; - /** - * コード(インライン) - */ "inlineCode": string; - /** - * プログラムなどのコードをインラインでシンタックスハイライトします。 - */ "inlineCodeDescription": string; - /** - * コード(ブロック) - */ "blockCode": string; - /** - * 複数行のプログラムなどのコードをブロックでシンタックスハイライトします。いくつかの言語を指定するとその言語に合わせたシンタックスハイライトになります。 - */ "blockCodeDescription": string; - /** - * 数式(インライン) - */ "inlineMath": string; - /** - * 数式(KaTeX)をインラインで表示します。 - */ "inlineMathDescription": string; - /** - * 数式(ブロック) - */ "blockMath": string; - /** - * 複数行の数式(KaTeX)をブロックで表示します。 - */ "blockMathDescription": string; - /** - * 引用 - */ "quote": string; - /** - * 内容が引用であることを示すことができます。 - */ "quoteDescription": string; - /** - * カスタム絵文字 - */ "emoji": string; - /** - * コロンでカスタム絵文字名を囲むと、カスタム絵文字を表示させることができます。 - */ "emojiDescription": string; - /** - * 検索 - */ "search": string; - /** - * 入力済み検索ボックスを表示させることができます。 - */ "searchDescription": string; - /** - * 反転 - */ "flip": string; - /** - * 内容を上下または左右に反転させます。 - */ "flipDescription": string; - /** - * アニメーション(びよんびよん) - */ "jelly": string; - /** - * びよんびよんするアニメーションを与えます。 - */ "jellyDescription": string; - /** - * アニメーション(じゃーん) - */ "tada": string; - /** - * ジャーン!という感じのアニメーションを与えます。 - */ "tadaDescription": string; - /** - * アニメーション(ジャンプ) - */ "jump": string; - /** - * 飛び跳ねるようなアニメーションを与えます。 - */ "jumpDescription": string; - /** - * アニメーション(バウンド) - */ "bounce": string; - /** - * ぽよんぽよん弾むようなアニメーションを与えます。 - */ "bounceDescription": string; - /** - * アニメーション(ぶるぶる) - */ "shake": string; - /** - * ぶるぶる震えるアニメーションを与えます。 - */ "shakeDescription": string; - /** - * アニメーション(ブレ) - */ "twitch": string; - /** - * 激しくブレるアニメーションを与えます。 - */ "twitchDescription": string; - /** - * アニメーション(回転) - */ "spin": string; - /** - * 回転するアニメーションを与えます。 - */ "spinDescription": string; - /** - * 大きく - */ "x2": string; - /** - * 内容を大きく表示します。 - */ "x2Description": string; - /** - * とても大きく - */ "x3": string; - /** - * 内容をとても大きく表示します。 - */ "x3Description": string; - /** - * 究極に大きく - */ "x4": string; - /** - * 内容を究極に大きく表示します。 - */ "x4Description": string; - /** - * ぼかし - */ "blur": string; - /** - * 内容をぼかすことができます。ポインターを上に乗せるとはっきり見えるようになります。 - */ "blurDescription": string; - /** - * フォント - */ "font": string; - /** - * 内容のフォントを指定することができます。 - */ "fontDescription": string; - /** - * レインボー - */ "rainbow": string; - /** - * 内容をレインボーにします。 - */ "rainbowDescription": string; - /** - * キラキラ - */ "sparkle": string; - /** - * キラキラしたパーティクルのエフェクトを追加します。 - */ "sparkleDescription": string; - /** - * フェード - */ "fade": string; - /** - * ゆっくりと現れたり消えたりする効果を追加します。 - */ "fadeDescription": string; - /** - * 回転 - */ "rotate": string; - /** - * 指定した角度で回転させます。 - */ "rotateDescription": string; - /** - * 位置 - */ "position": string; - /** - * 指定した位置に移動させます。 - */ "positionDescription": string; - /** - * スケール - */ "scale": string; - /** - * 指定した値で緩めます。 - */ "scaleDescription": string; - /** - * 描画色 - */ "fg": string; - /** - * 指定した値で描画色を指定します。 - */ "fgDescription": string; - /** - * 背景色 - */ "bg": string; - /** - * 指定した値で背景色を指定します。 - */ "bgDescription": string; - /** - * プレーン - */ "plain": string; - /** - * 内側の構文を全て無効にします。 - */ "plainDescription": string; - /** - * ルビ - */ "ruby": string; - /** - * 文字の上にルビを表示します。 - */ "rubyDescription": string; - /** - * ボーダー - */ - "border": string; - /** - * 内容を枠線で囲むことができます。 - */ - "borderDescription": string; }; "_instanceTicker": { - /** - * 表示しない - */ "none": string; - /** - * リモートユーザーに表示 - */ "remote": string; - /** - * 常に表示 - */ "always": string; }; "_serverDisconnectedBehavior": { - /** - * 自動でリロード - */ "reload": string; - /** - * ダイアログで警告 - */ "dialog": string; - /** - * 控えめに警告 - */ "quiet": string; - /** - * 警告なし - */ "none": string; }; "_channel": { - /** - * チャンネルを作成 - */ "create": string; - /** - * チャンネルを編集 - */ "edit": string; - /** - * バナーを設定 - */ "setBanner": string; - /** - * バナーを削除 - */ "removeBanner": string; - /** - * トレンド - */ "featured": string; - /** - * 管理中 - */ "owned": string; - /** - * フォロー中 - */ "following": string; - /** - * {n}人が参加中 - */ - "usersCount": ParameterizedString<"n">; - /** - * {n}投稿があります - */ - "notesCount": ParameterizedString<"n">; - /** - * 名前と説明 - */ + "usersCount": string; + "notesCount": string; "nameAndDescription": string; - /** - * 名前のみ - */ "nameOnly": string; - /** - * チャンネル外へのリノートと引用リノートを許可する - */ "allowRenoteToExternal": string; }; "_menuDisplay": { - /** - * 横 - */ "sideFull": string; - /** - * 横(アイコン) - */ "sideIcon": string; - /** - * 上部 - */ "top": string; - /** - * 隠す - */ "hide": string; }; "_wordMute": { - /** - * ミュートするワード - */ "muteWords": string; - /** - * スペースで区切るとAND指定になり、改行で区切るとOR指定になります。 - */ "muteWordsDescription": string; - /** - * キーワードをスラッシュで囲むと正規表現になります。 - */ "muteWordsDescription2": string; }; "_instanceMute": { - /** - * ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。 - */ "instanceMuteDescription": string; - /** - * 改行で区切って設定します - */ "instanceMuteDescription2": string; - /** - * 設定したサーバーのノートを隠します。 - */ "title": string; - /** - * ミュートするサーバー - */ "heading": string; }; "_theme": { - /** - * テーマを探す - */ "explore": string; - /** - * テーマのインストール - */ "install": string; - /** - * テーマの管理 - */ "manage": string; - /** - * テーマコード - */ "code": string; - /** - * 説明 - */ "description": string; - /** - * {name}をインストールしました - */ - "installed": ParameterizedString<"name">; - /** - * インストールされたテーマ - */ + "installed": string; "installedThemes": string; - /** - * 標準のテーマ - */ "builtinThemes": string; - /** - * そのテーマは既にインストールされています - */ "alreadyInstalled": string; - /** - * テーマの形式が間違っています - */ "invalid": string; - /** - * テーマを作る - */ "make": string; - /** - * ベース - */ "base": string; - /** - * 定数を追加 - */ "addConstant": string; - /** - * 定数 - */ "constant": string; - /** - * デフォルト値 - */ "defaultValue": string; - /** - * 色 - */ "color": string; - /** - * プロパティを参照 - */ "refProp": string; - /** - * 定数を参照 - */ "refConst": string; - /** - * キー - */ "key": string; - /** - * 関数 - */ "func": string; - /** - * 関数の種類 - */ "funcKind": string; - /** - * 引数 - */ "argument": string; - /** - * 元にするプロパティの名前 - */ "basedProp": string; - /** - * 不透明度 - */ "alpha": string; - /** - * 暗さ - */ "darken": string; - /** - * 明るさ - */ "lighten": string; - /** - * 定数名を入力してください - */ "inputConstantName": string; - /** - * ここにテーマコードを貼り付けて、エディターにインポートできます - */ "importInfo": string; - /** - * 定数 {const} を削除しても良いですか? - */ - "deleteConstantConfirm": ParameterizedString<"const">; + "deleteConstantConfirm": string; "keys": { - /** - * アクセント - */ "accent": string; - /** - * 背景 - */ "bg": string; - /** - * 文字 - */ "fg": string; - /** - * フォーカス - */ "focus": string; - /** - * インジケーター - */ "indicator": string; - /** - * パネル - */ "panel": string; - /** - * 影 - */ "shadow": string; - /** - * ヘッダー - */ "header": string; - /** - * サイドバーの背景 - */ "navBg": string; - /** - * サイドバーの文字 - */ "navFg": string; - /** - * サイドバー文字(ホバー) - */ "navHoverFg": string; - /** - * サイドバー文字(アクティブ) - */ "navActive": string; - /** - * サイドバーのインジケーター - */ "navIndicator": string; - /** - * リンク - */ "link": string; - /** - * ハッシュタグ - */ "hashtag": string; - /** - * メンション - */ "mention": string; - /** - * あなた宛てメンション - */ "mentionMe": string; - /** - * Renote - */ "renote": string; - /** - * モーダルの背景 - */ "modalBg": string; - /** - * 分割線 - */ "divider": string; - /** - * スクロールバーの取っ手 - */ "scrollbarHandle": string; - /** - * スクロールバーの取っ手(ホバー) - */ "scrollbarHandleHover": string; - /** - * 日付ラベルの文字 - */ "dateLabelFg": string; - /** - * 情報の背景 - */ "infoBg": string; - /** - * 情報の文字 - */ "infoFg": string; - /** - * 警告の背景 - */ "infoWarnBg": string; - /** - * 警告の文字 - */ "infoWarnFg": string; - /** - * 通知トーストの背景 - */ "toastBg": string; - /** - * 通知トーストの文字 - */ "toastFg": string; - /** - * ボタンの背景 - */ "buttonBg": string; - /** - * ボタンの背景 (ホバー) - */ "buttonHoverBg": string; - /** - * 入力ボックスの縁取り - */ "inputBorder": string; - /** - * リスト項目の背景 (ホバー) - */ "listItemHoverBg": string; - /** - * ドライブフォルダーの背景 - */ "driveFolderBg": string; - /** - * 壁紙のオーバーレイ - */ "wallpaperOverlay": string; - /** - * バッジ - */ "badge": string; - /** - * チャットの背景 - */ "messageBg": string; - /** - * アクセント (暗め) - */ "accentDarken": string; - /** - * アクセント (明るめ) - */ "accentLighten": string; - /** - * 強調された文字 - */ "fgHighlighted": string; }; }; "_sfx": { - /** - * ノート - */ "note": string; - /** - * ノート(自分) - */ "noteMy": string; - /** - * ノートを編集 - */ "noteEdited": string; - /** - * 通知 - */ "notification": string; - /** - * チャット - */ "chat": string; - /** - * チャット(バックグラウンド) - */ "chatBg": string; - /** - * リアクション選択時 - */ + "antenna": string; + "channel": string; "reaction": string; }; "_soundSettings": { - /** - * ドライブの音声を使用 - */ "driveFile": string; - /** - * ドライブのファイルを選択してください - */ "driveFileWarn": string; - /** - * このファイルは対応していません - */ "driveFileTypeWarn": string; - /** - * 音声ファイルを選択してください - */ "driveFileTypeWarnDescription": string; - /** - * 音声が長すぎます - */ "driveFileDurationWarn": string; - /** - * 長い音声を使用するとCherryPickの使用に支障をきたす可能性があります。それでも続行しますか? - */ "driveFileDurationWarnDescription": string; - /** - * 音声が読み込めませんでした。設定を変更してください - */ - "driveFileError": string; }; "_ago": { - /** - * 未来 - */ "future": string; - /** - * たった今 - */ "justNow": string; - /** - * {n}秒前 - */ - "secondsAgo": ParameterizedString<"n">; - /** - * {n}分前 - */ - "minutesAgo": ParameterizedString<"n">; - /** - * {n}時間前 - */ - "hoursAgo": ParameterizedString<"n">; - /** - * {n}日前 - */ - "daysAgo": ParameterizedString<"n">; - /** - * {n}週間前 - */ - "weeksAgo": ParameterizedString<"n">; - /** - * {n}ヶ月前 - */ - "monthsAgo": ParameterizedString<"n">; - /** - * {n}年前 - */ - "yearsAgo": ParameterizedString<"n">; - /** - * 日時の解析に失敗 - */ + "secondsAgo": string; + "minutesAgo": string; + "hoursAgo": string; + "daysAgo": string; + "weeksAgo": string; + "monthsAgo": string; + "yearsAgo": string; "invalid": string; }; "_timeIn": { - /** - * {n}秒後 - */ - "seconds": ParameterizedString<"n">; - /** - * {n}分後 - */ - "minutes": ParameterizedString<"n">; - /** - * {n}時間後 - */ - "hours": ParameterizedString<"n">; - /** - * {n}日後 - */ - "days": ParameterizedString<"n">; - /** - * {n}週間後 - */ - "weeks": ParameterizedString<"n">; - /** - * {n}ヶ月後 - */ - "months": ParameterizedString<"n">; - /** - * {n}年後 - */ - "years": ParameterizedString<"n">; + "seconds": string; + "minutes": string; + "hours": string; + "days": string; + "weeks": string; + "months": string; + "years": string; }; "_time": { - /** - * 秒 - */ "second": string; - /** - * 分 - */ "minute": string; - /** - * 時間 - */ "hour": string; - /** - * 日 - */ "day": string; }; "_2fa": { - /** - * 既に設定は完了しています。 - */ "alreadyRegistered": string; - /** - * 認証アプリの設定を開始 - */ "registerTOTP": string; - /** - * まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。 - */ - "step1": ParameterizedString<"a" | "b">; - /** - * 次に、表示されているQRコードをアプリでスキャンするか、ボタンをクリックして端末上でアプリを開きます。 - */ + "step1": string; "step2": string; - /** - * デスクトップアプリを使用する場合は次のURIを入力します - */ + "step2Click": string; "step2Uri": string; - /** - * 確認コードを入力 - */ "step3Title": string; - /** - * アプリに表示されている確認コード(トークン)を入力します。 - */ "step3": string; - /** - * 設定が完了しました - */ "setupCompleted": string; - /** - * これからログインするときも、同じようにコードを入力します。 - */ "step4": string; - /** - * お使いのブラウザはセキュリティキーに対応していません。 - */ "securityKeyNotSupported": string; - /** - * セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。 - */ "registerTOTPBeforeKey": string; - /** - * FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。 - */ "securityKeyInfo": string; - /** - * セキュリティキー・パスキーを登録する - */ "registerSecurityKey": string; - /** - * キーの名前を入力 - */ "securityKeyName": string; - /** - * ブラウザの指示に従い、セキュリティキーやパスキーを登録してください - */ "tapSecurityKey": string; - /** - * セキュリティキーを削除 - */ "removeKey": string; - /** - * {name}を削除しますか? - */ - "removeKeyConfirm": ParameterizedString<"name">; - /** - * セキュリティキーが登録されている場合、認証アプリの設定は解除できません。 - */ + "removeKeyConfirm": string; "whyTOTPOnlyRenew": string; - /** - * 認証アプリを再設定 - */ "renewTOTP": string; - /** - * 今までの認証アプリの確認コードおよびバックアップコードは使用できなくなります - */ "renewTOTPConfirm": string; - /** - * 再設定する - */ "renewTOTPOk": string; - /** - * やめておく - */ "renewTOTPCancel": string; - /** - * このウィザードを閉じる前に、以下のバックアップコードを確認してください。 - */ "checkBackupCodesBeforeCloseThisWizard": string; - /** - * バックアップコード - */ "backupCodes": string; - /** - * 認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。 - */ "backupCodesDescription": string; - /** - * バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。 - */ "backupCodeUsedWarning": string; - /** - * バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。 - */ "backupCodesExhaustedWarning": string; - /** - * 詳細なガイドはこちら - */ - "moreDetailedGuideHere": string; }; "_permissions": { - /** - * アカウントの情報を見る - */ "read:account": string; - /** - * アカウントの情報を変更する - */ "write:account": string; - /** - * ブロックを見る - */ "read:blocks": string; - /** - * ブロックを操作する - */ "write:blocks": string; - /** - * ドライブを見る - */ "read:drive": string; - /** - * ドライブを操作する - */ "write:drive": string; - /** - * お気に入りを見る - */ "read:favorites": string; - /** - * お気に入りを操作する - */ "write:favorites": string; - /** - * フォローの情報を見る - */ "read:following": string; - /** - * フォロー・フォロー解除する - */ "write:following": string; - /** - * チャットを見る - */ "read:messaging": string; - /** - * チャットを操作する - */ "write:messaging": string; - /** - * ミュートを見る - */ "read:mutes": string; - /** - * ミュートを操作する - */ "write:mutes": string; - /** - * ノートを作成・削除する - */ "write:notes": string; - /** - * 通知を見る - */ "read:notifications": string; - /** - * 通知を操作する - */ "write:notifications": string; - /** - * リアクションを見る - */ "read:reactions": string; - /** - * リアクションを操作する - */ "write:reactions": string; - /** - * 投票する - */ "write:votes": string; - /** - * ページを見る - */ "read:pages": string; - /** - * ページを操作する - */ "write:pages": string; - /** - * ページのいいねを見る - */ "read:page-likes": string; - /** - * ページのいいねを操作する - */ "write:page-likes": string; - /** - * ユーザーグループを見る - */ "read:user-groups": string; - /** - * ユーザーグループを操作する - */ "write:user-groups": string; - /** - * チャンネルを見る - */ "read:channels": string; - /** - * チャンネルを操作する - */ "write:channels": string; - /** - * ギャラリーを見る - */ "read:gallery": string; - /** - * ギャラリーを操作する - */ "write:gallery": string; - /** - * ギャラリーのいいねを見る - */ "read:gallery-likes": string; - /** - * ギャラリーのいいねを操作する - */ "write:gallery-likes": string; - /** - * Playを見る - */ "read:flash": string; - /** - * Playを操作する - */ "write:flash": string; - /** - * Playのいいねを見る - */ "read:flash-likes": string; - /** - * Playのいいねを操作する - */ "write:flash-likes": string; - /** - * ユーザーからの通報を見る - */ "read:admin:abuse-user-reports": string; - /** - * ユーザーアカウントを削除する - */ "write:admin:delete-account": string; - /** - * ユーザーのすべてのファイルを削除する - */ "write:admin:delete-all-files-of-a-user": string; - /** - * データベースインデックスに関する情報を見る - */ "read:admin:index-stats": string; - /** - * データベーステーブルに関する情報を見る - */ "read:admin:table-stats": string; - /** - * ユーザーのIPアドレスを見る - */ "read:admin:user-ips": string; - /** - * インスタンスのメタデータを見る - */ "read:admin:meta": string; - /** - * ユーザーのパスワードをリセットする - */ "write:admin:reset-password": string; - /** - * ユーザーからの通報を解決する - */ "write:admin:resolve-abuse-user-report": string; - /** - * メールを送る - */ "write:admin:send-email": string; - /** - * サーバーの情報を見る - */ "read:admin:server-info": string; - /** - * モデレーションログを見る - */ "read:admin:show-moderation-log": string; - /** - * ユーザーのプライベートな情報を見る - */ "read:admin:show-user": string; - /** - * ユーザーを凍結する - */ + "read:admin:show-users": string; "write:admin:suspend-user": string; - /** - * ユーザーのアバターを削除する - */ "write:admin:unset-user-avatar": string; - /** - * ユーザーのバーナーを削除する - */ "write:admin:unset-user-banner": string; - /** - * ユーザーの凍結を解除する - */ "write:admin:unsuspend-user": string; - /** - * インスタンスのメタデータを操作する - */ "write:admin:meta": string; - /** - * モデレーションノートを操作する - */ "write:admin:user-note": string; - /** - * ロールを操作する - */ "write:admin:roles": string; - /** - * ロールを見る - */ "read:admin:roles": string; - /** - * リレーを操作する - */ "write:admin:relays": string; - /** - * リレーを見る - */ "read:admin:relays": string; - /** - * 招待コードを操作する - */ "write:admin:invite-codes": string; - /** - * 招待コードを見る - */ "read:admin:invite-codes": string; - /** - * お知らせを操作する - */ "write:admin:announcements": string; - /** - * お知らせを見る - */ "read:admin:announcements": string; - /** - * アバターデコレーションを操作する - */ "write:admin:avatar-decorations": string; - /** - * アバターデコレーションを見る - */ "read:admin:avatar-decorations": string; - /** - * 連合に関する情報を操作する - */ "write:admin:federation": string; - /** - * ユーザーアカウントを操作する - */ "write:admin:account": string; - /** - * ユーザーに関する情報を見る - */ "read:admin:account": string; - /** - * 絵文字を操作する - */ "write:admin:emoji": string; - /** - * 絵文字を見る - */ "read:admin:emoji": string; - /** - * ジョブキューを操作する - */ "write:admin:queue": string; - /** - * ジョブキューに関する情報を見る - */ "read:admin:queue": string; - /** - * プロモーションノートを操作する - */ "write:admin:promo": string; - /** - * ユーザーのドライブを操作する - */ "write:admin:drive": string; - /** - * ユーザーのドライブの関する情報を見る - */ "read:admin:drive": string; - /** - * 管理者用のWebsocket APIを使う - */ "read:admin:stream": string; - /** - * 広告を操作する - */ "write:admin:ad": string; - /** - * 広告を見る - */ "read:admin:ad": string; - /** - * 招待コードを作成する - */ "write:invite-codes": string; - /** - * 招待コードを取得する - */ "read:invite-codes": string; - /** - * クリップのいいねを操作する - */ "write:clip-favorite": string; - /** - * クリップのいいねを見る - */ "read:clip-favorite": string; - /** - * 連合に関する情報を取得する - */ "read:federation": string; - /** - * 違反を報告する - */ "write:report-abuse": string; }; "_auth": { - /** - * アプリへのアクセス許可 - */ "shareAccessTitle": string; - /** - * 「{name}」がアカウントにアクセスすることを許可しますか? - */ - "shareAccess": ParameterizedString<"name">; - /** - * アカウントへのアクセスを許可しますか? - */ + "shareAccess": string; "shareAccessAsk": string; - /** - * {name}は次の権限を要求しています - */ - "permission": ParameterizedString<"name">; - /** - * このアプリは次の権限を要求しています - */ + "permission": string; "permissionAsk": string; - /** - * アプリケーションに戻ってやっていってください - */ "pleaseGoBack": string; - /** - * アプリケーションに戻っています - */ "callback": string; - /** - * アクセスを拒否しました - */ "denied": string; - /** - * アプリケーションにアクセス許可を与えるには、ログインが必要です。 - */ "pleaseLogin": string; }; "_antennaSources": { - /** - * 全てのノート - */ "all": string; - /** - * フォローしているユーザーのノート - */ "homeTimeline": string; - /** - * 指定した一人または複数のユーザーのノート - */ "users": string; - /** - * 指定したリストのユーザーのノート - */ "userList": string; - /** - * 指定したグループのユーザーのノート - */ "userGroup": string; - /** - * 指定した一人または複数のユーザーを除いた全てのノート - */ "userBlacklist": string; }; "_weekday": { - /** - * 日曜日 - */ "sunday": string; - /** - * 月曜日 - */ "monday": string; - /** - * 火曜日 - */ "tuesday": string; - /** - * 水曜日 - */ "wednesday": string; - /** - * 木曜日 - */ "thursday": string; - /** - * 金曜日 - */ "friday": string; - /** - * 土曜日 - */ "saturday": string; }; "_widgets": { - /** - * プロフィール - */ "profile": string; - /** - * サーバー情報 - */ "instanceInfo": string; - /** - * 付箋 - */ "memo": string; - /** - * 通知 - */ "notifications": string; - /** - * タイムライン - */ "timeline": string; - /** - * カレンダー - */ "calendar": string; - /** - * トレンド - */ "trends": string; - /** - * 時計 - */ "clock": string; - /** - * RSSリーダー - */ "rss": string; - /** - * RSSティッカー - */ "rssTicker": string; - /** - * アクティビティ - */ "activity": string; - /** - * フォト - */ "photos": string; - /** - * デジタル時計 - */ "digitalClock": string; - /** - * UNIX時計 - */ "unixClock": string; - /** - * 連合 - */ "federation": string; - /** - * サーバークラウド - */ "instanceCloud": string; - /** - * 投稿フォーム - */ "postForm": string; - /** - * スライドショー - */ "slideshow": string; - /** - * ボタン - */ "button": string; - /** - * オンラインユーザー - */ "onlineUsers": string; - /** - * ジョブキュー - */ "jobQueue": string; - /** - * サーバーメトリクス - */ "serverMetric": string; - /** - * AiScriptコンソール - */ "aiscript": string; - /** - * AiScript App - */ "aiscriptApp": string; - /** - * 藍 - */ "aichan": string; - /** - * ユーザーリスト - */ "userList": string; "_userList": { - /** - * リストを選択 - */ "chooseList": string; }; - /** - * クリッカー - */ "clicker": string; - /** - * 今日誕生日のユーザー - */ "birthdayFollowings": string; - /** - * サイコロ - */ - "dice": string; - /** - * 検索 - */ - "search": string; }; "_cw": { - /** - * 隠す - */ "hide": string; - /** - * もっと見る - */ "show": string; - /** - * {count}文字 - */ - "chars": ParameterizedString<"count">; - /** - * {count}ファイル - */ - "files": ParameterizedString<"count">; + "chars": string; + "files": string; }; "_poll": { - /** - * 選択肢は最低2つ必要です - */ "noOnlyOneChoice": string; - /** - * 選択肢{n} - */ - "choiceN": ParameterizedString<"n">; - /** - * これ以上追加できません - */ + "choiceN": string; "noMore": string; - /** - * 複数回答可 - */ "canMultipleVote": string; - /** - * 期限 - */ "expiration": string; - /** - * 無期限 - */ "infinite": string; - /** - * 日時指定 - */ "at": string; - /** - * 経過指定 - */ "after": string; - /** - * 期日 - */ "deadlineDate": string; - /** - * 時間 - */ "deadlineTime": string; - /** - * 期間 - */ "duration": string; - /** - * {n}票 - */ - "votesCount": ParameterizedString<"n">; - /** - * 計{n}票 - */ - "totalVotes": ParameterizedString<"n">; - /** - * 投票する - */ + "votesCount": string; + "totalVotes": string; "vote": string; - /** - * 結果を見る - */ "showResult": string; - /** - * 投票済み - */ "voted": string; - /** - * 終了済み - */ "closed": string; - /** - * 終了まであと{d}日{h}時間 - */ - "remainingDays": ParameterizedString<"d" | "h">; - /** - * 終了まであと{h}時間{m}分 - */ - "remainingHours": ParameterizedString<"h" | "m">; - /** - * 終了まであと{m}分{s}秒 - */ - "remainingMinutes": ParameterizedString<"m" | "s">; - /** - * 終了まであと{s}秒 - */ - "remainingSeconds": ParameterizedString<"s">; + "remainingDays": string; + "remainingHours": string; + "remainingMinutes": string; + "remainingSeconds": string; }; "_visibility": { - /** - * パブリック - */ "public": string; - /** - * 全てのユーザーに公開 - */ "publicDescription": string; - /** - * ホーム - */ "home": string; - /** - * ホームタイムラインのみに公開 - */ "homeDescription": string; - /** - * フォロワー - */ "followers": string; - /** - * 自分のフォロワーのみに公開 - */ "followersDescription": string; - /** - * ダイレクト - */ "specified": string; - /** - * 指定したユーザーのみに公開 - */ "specifiedDescription": string; - /** - * プライベート - */ - "private": string; - /** - * あなたのみが見れます - */ - "privateDescription": string; - /** - * 連合なし - */ "disableFederation": string; - /** - * 他サーバーへの配信を行いません - */ "disableFederationDescription": string; }; "_postForm": { - /** - * ノートを作成するにはログインが必要です。 - */ "signinRequiredPlaceholder": string; - /** - * このノートに返信... - */ "replyPlaceholder": string; - /** - * このノートを引用... - */ "quotePlaceholder": string; - /** - * チャンネルに投稿... - */ "channelPlaceholder": string; "_placeholders": { - /** - * いまどうしてる? - */ "a": string; - /** - * 何かありましたか? - */ "b": string; - /** - * 何をお考えですか? - */ "c": string; - /** - * 言いたいことは? - */ "d": string; - /** - * ここに書いてください - */ "e": string; - /** - * あなたが書くのを待っています... - */ "f": string; }; }; "_profile": { - /** - * 名前 - */ "name": string; - /** - * ユーザー名 - */ "username": string; - /** - * 自己紹介 - */ "description": string; - /** - * ハッシュタグを含めることができます。 - */ "youCanIncludeHashtags": string; - /** - * 追加情報 - */ "metadata": string; - /** - * 追加情報を編集 - */ "metadataEdit": string; - /** - * プロフィールに表として追加情報を表示することができます。 - */ "metadataDescription": string; - /** - * ラベル - */ "metadataLabel": string; - /** - * 内容 - */ "metadataContent": string; - /** - * アイコン画像を変更 - */ "changeAvatar": string; - /** - * バナー画像を変更 - */ "changeBanner": string; - /** - * 内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。 - */ "verifiedLinkDescription": string; - /** - * 最大{max}つまでデコレーションを付けられます。 - */ - "avatarDecorationMax": ParameterizedString<"max">; + "avatarDecorationMax": string; }; "_exportOrImport": { - /** - * 全てのノート - */ "allNotes": string; - /** - * お気に入りにしたノート - */ "favoritedNotes": string; - /** - * クリップ - */ - "clips": string; - /** - * フォロー - */ "followingList": string; - /** - * ミュート - */ "muteList": string; - /** - * ブロック - */ "blockingList": string; - /** - * リスト - */ "userLists": string; - /** - * ミュートしているユーザーを除外 - */ "excludeMutingUsers": string; - /** - * 使われていないアカウントを除外 - */ "excludeInactiveUsers": string; - /** - * インポートした人による返信をTLに含むようにする - */ "withReplies": string; }; "_charts": { - /** - * 連合 - */ "federation": string; - /** - * リクエスト - */ "apRequest": string; - /** - * ユーザーの増減 - */ "usersIncDec": string; - /** - * ユーザーの合計 - */ "usersTotal": string; - /** - * アクティブユーザー数 - */ "activeUsers": string; - /** - * ノートの増減 - */ "notesIncDec": string; - /** - * ローカルのノートの増減 - */ "localNotesIncDec": string; - /** - * リモートのノートの増減 - */ "remoteNotesIncDec": string; - /** - * ノートの合計 - */ "notesTotal": string; - /** - * ファイルの増減 - */ "filesIncDec": string; - /** - * ファイルの合計 - */ "filesTotal": string; - /** - * ストレージ使用量の増減 - */ "storageUsageIncDec": string; - /** - * ストレージ使用量の合計 - */ "storageUsageTotal": string; }; "_instanceCharts": { - /** - * リクエスト - */ "requests": string; - /** - * ユーザーの増減 - */ "users": string; - /** - * ユーザーの累積 - */ "usersTotal": string; - /** - * ノートの増減 - */ "notes": string; - /** - * ノートの累積 - */ "notesTotal": string; - /** - * フォロー/フォロワーの増減 - */ "ff": string; - /** - * フォロー/フォロワーの累積 - */ "ffTotal": string; - /** - * キャッシュサイズの増減 - */ "cacheSize": string; - /** - * キャッシュサイズの累積 - */ "cacheSizeTotal": string; - /** - * ファイル数の増減 - */ "files": string; - /** - * ファイル数の累積 - */ "filesTotal": string; }; "_timelines": { - /** - * ホーム - */ "home": string; - /** - * ローカル - */ "local": string; - /** - * メディア - */ - "media": string; - /** - * ソーシャル - */ "social": string; - /** - * グローバル - */ "global": string; }; "_play": { - /** - * Playの作成 - */ "new": string; - /** - * Playの編集 - */ "edit": string; - /** - * Playを作成しました - */ "created": string; - /** - * Playを更新しました - */ "updated": string; - /** - * Playを削除しました - */ "deleted": string; - /** - * Play設定 - */ "pageSetting": string; - /** - * このPlayを編集 - */ "editThisPage": string; - /** - * ソースを表示 - */ "viewSource": string; - /** - * 自分のPlay - */ "my": string; - /** - * いいねしたPlay - */ "liked": string; - /** - * 人気 - */ "featured": string; - /** - * タイトル - */ "title": string; - /** - * スクリプト - */ "script": string; - /** - * 説明 - */ "summary": string; - /** - * 非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。 - */ - "visibilityDescription": string; }; "_pages": { - /** - * ページの作成 - */ "newPage": string; - /** - * ページの編集 - */ "editPage": string; - /** - * ソースを表示中 - */ "readPage": string; - /** - * ページを作成しました - */ "created": string; - /** - * ページを更新しました - */ "updated": string; - /** - * ページを削除しました - */ "deleted": string; - /** - * ページ設定 - */ "pageSetting": string; - /** - * 指定されたページURLは既に存在しています - */ "nameAlreadyExists": string; - /** - * 不正なページURLです - */ "invalidNameTitle": string; - /** - * 空白でないか確認してください - */ "invalidNameText": string; - /** - * このページを編集 - */ "editThisPage": string; - /** - * ソースを表示 - */ "viewSource": string; - /** - * ページを見る - */ "viewPage": string; - /** - * いいね - */ "like": string; - /** - * いいね解除 - */ "unlike": string; - /** - * 自分のページ - */ "my": string; - /** - * いいねしたページ - */ "liked": string; - /** - * 人気 - */ "featured": string; - /** - * インスペクター - */ "inspector": string; - /** - * コンテンツ - */ "contents": string; - /** - * ページブロック - */ "content": string; - /** - * 変数 - */ "variables": string; - /** - * タイトル - */ "title": string; - /** - * ページURL - */ "url": string; - /** - * ページの要約 - */ "summary": string; - /** - * 中央寄せ - */ "alignCenter": string; - /** - * ピン留めされているときにタイトルを非表示 - */ "hideTitleWhenPinned": string; - /** - * フォント - */ "font": string; - /** - * セリフ - */ "fontSerif": string; - /** - * サンセリフ - */ "fontSansSerif": string; - /** - * アイキャッチ画像を設定 - */ "eyeCatchingImageSet": string; - /** - * アイキャッチ画像を削除 - */ "eyeCatchingImageRemove": string; - /** - * ブロックを追加 - */ "chooseBlock": string; - /** - * 種類を選択 - */ "selectType": string; - /** - * コンテンツ - */ "contentBlocks": string; - /** - * 入力 - */ "inputBlocks": string; - /** - * 特殊 - */ "specialBlocks": string; "blocks": { - /** - * テキスト - */ "text": string; - /** - * テキストエリア - */ "textarea": string; - /** - * セクション - */ "section": string; - /** - * 画像 - */ "image": string; - /** - * ボタン - */ "button": string; - /** - * 動的ブロック - */ - "dynamic": string; - /** - * このブロックは廃止されています。今後は{play}を利用してください。 - */ - "dynamicDescription": ParameterizedString<"play">; - /** - * ノート埋め込み - */ "note": string; "_note": { - /** - * ノートID - */ "id": string; - /** - * ノートURLをペーストして設定することもできます。 - */ "idDescription": string; - /** - * 詳細な表示 - */ "detailed": string; }; }; }; "_relayStatus": { - /** - * 承認待ち - */ "requesting": string; - /** - * 承認済み - */ "accepted": string; - /** - * 拒否済み - */ "rejected": string; }; "_notification": { - /** - * ファイルがアップロードされました - */ "fileUploaded": string; - /** - * {name}からのメンション - */ - "youGotMention": ParameterizedString<"name">; - /** - * {name}からのリプライ - */ - "youGotReply": ParameterizedString<"name">; - /** - * {name}による引用 - */ - "youGotQuote": ParameterizedString<"name">; - /** - * {name}が反応しました - */ - "youGotReact": ParameterizedString<"name">; - /** - * {name}がRenoteしました - */ - "youRenoted": ParameterizedString<"name">; - /** - * フォローされました - */ + "youGotMention": string; + "youGotReply": string; + "youGotQuote": string; + "youGotReact": string; + "youRenoted": string; "youWereFollowed": string; - /** - * フォローリクエストが来ました - */ "youReceivedFollowRequest": string; - /** - * フォローリクエストが承認されました - */ "yourFollowRequestAccepted": string; - /** - * {userName}があなたをグループに招待しました - */ - "youWereInvitedToGroup": ParameterizedString<"userName">; - /** - * アンケートの結果が出ました - */ + "youWereInvitedToGroup": string; "pollEnded": string; - /** - * 新しい投稿 - */ "newNote": string; - /** - * アンテナ {name} - */ - "unreadAntennaNote": ParameterizedString<"name">; - /** - * ロールが付与されました - */ + "unreadAntennaNote": string; "roleAssigned": string; - /** - * プッシュ通知の更新をしました - */ "emptyPushNotificationMessage": string; - /** - * 実績を獲得 - */ "achievementEarned": string; - /** - * 通知テスト - */ "testNotification": string; - /** - * 通知の表示を確かめる - */ "checkNotificationBehavior": string; - /** - * テスト通知を送信する - */ "sendTestNotification": string; - /** - * 通知はこのように表示されます - */ "notificationWillBeDisplayedLikeThis": string; - /** - * {n}人がリアクションしました - */ - "reactedBySomeUsers": ParameterizedString<"n">; - /** - * {n}人がいいねしました - */ - "likedBySomeUsers": ParameterizedString<"n">; - /** - * {n}人がリノートしました - */ - "renotedBySomeUsers": ParameterizedString<"n">; - /** - * {n}人にフォローされました - */ - "followedBySomeUsers": ParameterizedString<"n">; - /** - * 通知の履歴をリセットする - */ - "flushNotification": string; + "reactedBySomeUsers": string; + "renotedBySomeUsers": string; + "followedBySomeUsers": string; "_types": { - /** - * すべて - */ "all": string; - /** - * ユーザーの新規投稿 - */ "note": string; - /** - * フォロー - */ "follow": string; - /** - * メンション - */ "mention": string; - /** - * リプライ - */ "reply": string; - /** - * Renote - */ "renote": string; - /** - * 引用 - */ "quote": string; - /** - * リアクション - */ "reaction": string; - /** - * アンケートが終了 - */ "pollEnded": string; - /** - * フォロー申請を受け取った - */ "receiveFollowRequest": string; - /** - * フォローが受理された - */ "followRequestAccepted": string; - /** - * グループに招待された - */ "groupInvited": string; - /** - * ロールが付与された - */ "roleAssigned": string; - /** - * 実績の獲得 - */ "achievementEarned": string; - /** - * 連携アプリからの通知 - */ "app": string; }; "_actions": { - /** - * フォローバック - */ "followBack": string; - /** - * 返信 - */ "reply": string; - /** - * Renote - */ "renote": string; }; }; "_deck": { - /** - * 常にメインカラムを表示 - */ "alwaysShowMainColumn": string; - /** - * カラムの寄せ - */ "columnAlign": string; - /** - * カラムを追加 - */ "addColumn": string; - /** - * 新着ノート通知の設定 - */ - "newNoteNotificationSettings": string; - /** - * カラムの設定 - */ "configureColumn": string; - /** - * 左に移動 - */ "swapLeft": string; - /** - * 右に移動 - */ "swapRight": string; - /** - * 上に移動 - */ "swapUp": string; - /** - * 下に移動 - */ "swapDown": string; - /** - * 左にスタック - */ "stackLeft": string; - /** - * 右に出す - */ "popRight": string; - /** - * プロファイル - */ "profile": string; - /** - * 新規プロファイル - */ "newProfile": string; - /** - * プロファイルを削除 - */ "deleteProfile": string; - /** - * カラムを組み合わせて自分だけのインターフェイスを作りましょう! - */ "introduction": string; - /** - * 画面の右にある + を押して、いつでもカラムを追加できます。 - */ "introduction2": string; - /** - * カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください - */ "widgetsIntroduction": string; - /** - * 非ルートページは簡易UIで表示 - */ "useSimpleUiForNonRootPages": string; - /** - * 「幅を自動調整」が有効の場合、これが幅の最小値となります - */ "usedAsMinWidthWhenFlexible": string; - /** - * 幅を自動調整 - */ "flexible": string; "_columns": { - /** - * メイン - */ "main": string; - /** - * ウィジェット - */ "widgets": string; - /** - * 通知 - */ "notifications": string; - /** - * タイムライン - */ "tl": string; - /** - * アンテナ - */ "antenna": string; - /** - * リスト - */ "list": string; - /** - * チャンネル - */ "channel": string; - /** - * あなた宛て - */ "mentions": string; - /** - * ダイレクト - */ "direct": string; - /** - * ロールタイムライン - */ "roleTimeline": string; }; }; "_dialog": { - /** - * 最大文字数を超えています! 現在 {current} / 制限 {max} - */ - "charactersExceeded": ParameterizedString<"current" | "max">; - /** - * 最小文字数を下回っています! 現在 {current} / 制限 {min} - */ - "charactersBelow": ParameterizedString<"current" | "min">; + "charactersExceeded": string; + "charactersBelow": string; }; "_disabledTimeline": { - /** - * 無効化されたタイムライン - */ "title": string; - /** - * 現在のロールでは、このタイムラインを使用することはできません。 - */ "description": string; }; "_drivecleaner": { - /** - * サイズが大きい順 - */ "orderBySizeDesc": string; - /** - * 追加日が古い順 - */ "orderByCreatedAtAsc": string; }; "_webhookSettings": { - /** - * Webhookを作成 - */ "createWebhook": string; - /** - * Webhookを編集 - */ - "modifyWebhook": string; - /** - * 名前 - */ "name": string; - /** - * シークレット - */ "secret": string; - /** - * トリガー - */ - "trigger": string; - /** - * 有効 - */ + "events": string; "active": string; "_events": { - /** - * フォローしたとき - */ "follow": string; - /** - * フォローされたとき - */ "followed": string; - /** - * ノートを投稿したとき - */ "note": string; - /** - * 返信されたとき - */ "reply": string; - /** - * Renoteされたとき - */ "renote": string; - /** - * リアクションがあったとき - */ "reaction": string; - /** - * メンションされたとき - */ "mention": string; }; - "_systemEvents": { - /** - * ユーザーから通報があったとき - */ - "abuseReport": string; - /** - * ユーザーからの通報を処理したとき - */ - "abuseReportResolved": string; - /** - * ユーザーが作成されたとき - */ - "userCreated": string; - }; - /** - * Webhookを削除しますか? - */ - "deleteConfirm": string; - }; - "_abuseReport": { - "_notificationRecipient": { - /** - * 通報の通知先を追加 - */ - "createRecipient": string; - /** - * 通報の通知先を編集 - */ - "modifyRecipient": string; - /** - * 通知先の種類 - */ - "recipientType": string; - "_recipientType": { - /** - * メール - */ - "mail": string; - /** - * Webhook - */ - "webhook": string; - "_captions": { - /** - * モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ) - */ - "mail": string; - /** - * 指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信) - */ - "webhook": string; - }; - }; - /** - * キーワード - */ - "keywords": string; - /** - * 通知先ユーザー - */ - "notifiedUser": string; - /** - * 使用するWebhook - */ - "notifiedWebhook": string; - /** - * 通知先を削除しますか? - */ - "deleteConfirm": string; - }; }; "_moderationLogTypes": { - /** - * ロールを作成 - */ "createRole": string; - /** - * ロールを削除 - */ "deleteRole": string; - /** - * ロールを更新 - */ "updateRole": string; - /** - * ロールへアサイン - */ "assignRole": string; - /** - * ロールのアサイン解除 - */ "unassignRole": string; - /** - * 凍結 - */ "suspend": string; - /** - * 凍結解除 - */ "unsuspend": string; - /** - * カスタム絵文字追加 - */ "addCustomEmoji": string; - /** - * カスタム絵文字更新 - */ "updateCustomEmoji": string; - /** - * カスタム絵文字削除 - */ "deleteCustomEmoji": string; - /** - * サーバー設定更新 - */ "updateServerSettings": string; - /** - * ユーザーのモデレーションノート更新 - */ "updateUserNote": string; - /** - * ファイルを削除 - */ "deleteDriveFile": string; - /** - * ノートを削除 - */ "deleteNote": string; - /** - * 全体のお知らせを作成 - */ "createGlobalAnnouncement": string; - /** - * ユーザーへお知らせを作成 - */ "createUserAnnouncement": string; - /** - * 全体のお知らせを更新 - */ "updateGlobalAnnouncement": string; - /** - * ユーザーのお知らせを更新 - */ "updateUserAnnouncement": string; - /** - * 全体のお知らせを削除 - */ "deleteGlobalAnnouncement": string; - /** - * ユーザーのお知らせを削除 - */ "deleteUserAnnouncement": string; - /** - * パスワードをリセット - */ "resetPassword": string; - /** - * リモートサーバーを停止 - */ "suspendRemoteInstance": string; - /** - * リモートサーバーを再開 - */ "unsuspendRemoteInstance": string; - /** - * リモートサーバーのモデレーションノート更新 - */ - "updateRemoteInstanceNote": string; - /** - * ファイルをセンシティブ付与 - */ "markSensitiveDriveFile": string; - /** - * ファイルをセンシティブ解除 - */ "unmarkSensitiveDriveFile": string; - /** - * 通報を解決 - */ "resolveAbuseReport": string; - /** - * 招待コードを作成 - */ "createInvitation": string; - /** - * 広告を作成 - */ "createAd": string; - /** - * 広告を削除 - */ "deleteAd": string; - /** - * 広告を更新 - */ "updateAd": string; - /** - * アイコンデコレーションを作成 - */ "createAvatarDecoration": string; - /** - * アイコンデコレーションを更新 - */ "updateAvatarDecoration": string; - /** - * アイコンデコレーションを削除 - */ "deleteAvatarDecoration": string; - /** - * ユーザーのアイコンを解除 - */ "unsetUserAvatar": string; - /** - * ユーザーのバナーを解除 - */ "unsetUserBanner": string; - /** - * SystemWebhookを作成 - */ - "createSystemWebhook": string; - /** - * SystemWebhookを更新 - */ - "updateSystemWebhook": string; - /** - * SystemWebhookを削除 - */ - "deleteSystemWebhook": string; - /** - * 通報の通知先を作成 - */ - "createAbuseReportNotificationRecipient": string; - /** - * 通報の通知先を更新 - */ - "updateAbuseReportNotificationRecipient": string; - /** - * 通報の通知先を削除 - */ - "deleteAbuseReportNotificationRecipient": string; }; "_fileViewer": { - /** - * ファイルの詳細 - */ "title": string; - /** - * ファイルタイプ - */ "type": string; - /** - * ファイルサイズ - */ "size": string; - /** - * URL - */ "url": string; - /** - * 追加日 - */ "uploadedAt": string; - /** - * 添付されているノート - */ "attachedNotes": string; - /** - * このページは、このファイルをアップロードしたユーザーしか閲覧できません。 - */ "thisPageCanBeSeenFromTheAuthor": string; }; "_externalResourceInstaller": { - /** - * 外部サイトからインストール - */ "title": string; - /** - * 配布元が信頼できるかを確認した上でインストールしてください。 - */ "checkVendorBeforeInstall": string; "_plugin": { - /** - * このプラグインをインストールしますか? - */ "title": string; - /** - * プラグイン情報 - */ "metaTitle": string; }; "_theme": { - /** - * このテーマをインストールしますか? - */ "title": string; - /** - * テーマ情報 - */ "metaTitle": string; }; "_meta": { - /** - * 基本のカラースキーム - */ "base": string; }; "_vendorInfo": { - /** - * 配布元情報 - */ "title": string; - /** - * 参照したエンドポイント - */ "endpoint": string; - /** - * ファイル整合性の確認 - */ "hashVerify": string; - /** - * 確認済み - */ "hashVerified": string; }; "_errors": { "_invalidParams": { - /** - * パラメータが不足しています - */ "title": string; - /** - * 外部サイトからデータを取得するために必要な情報が不足しています。URLをお確かめください。 - */ "description": string; }; "_resourceTypeNotSupported": { - /** - * この外部リソースには対応していません - */ "title": string; - /** - * この外部サイトから取得したリソースの種別には対応していません。サイト管理者にお問い合わせください。 - */ "description": string; }; "_failedToFetch": { - /** - * データの取得に失敗しました - */ "title": string; - /** - * 外部サイトとの通信に失敗しました。もう一度試しても改善しない場合、サイト管理者にお問い合わせください。 - */ "fetchErrorDescription": string; - /** - * 外部サイトから取得したデータが読み取れませんでした。サイト管理者にお問い合わせください。 - */ "parseErrorDescription": string; }; "_hashUnmatched": { - /** - * 正しいデータが取得できませんでした - */ "title": string; - /** - * 提供されたデータの整合性の確認に失敗しました。セキュリティ上、インストールは続行できません。サイト管理者にお問い合わせください。 - */ "description": string; }; "_pluginParseFailed": { - /** - * AiScript エラー - */ "title": string; - /** - * データは取得できたものの、AiScriptの解析時にエラーがあったため読み込めませんでした。プラグインの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。 - */ "description": string; }; "_pluginInstallFailed": { - /** - * プラグインのインストールに失敗しました - */ "title": string; - /** - * プラグインのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。 - */ "description": string; }; "_themeParseFailed": { - /** - * テーマ解析エラー - */ "title": string; - /** - * データは取得できたものの、テーマファイルの解析時にエラーがあったため読み込めませんでした。テーマの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。 - */ "description": string; }; "_themeInstallFailed": { - /** - * テーマのインストールに失敗しました - */ "title": string; - /** - * テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。 - */ "description": string; }; }; }; "_dataSaver": { "_media": { - /** - * メディアの読み込みを無効化 - */ "title": string; - /** - * 画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。 - */ "description": string; }; "_avatar": { - /** - * アイコン画像のアニメーションを無効化 - */ "title": string; - /** - * アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。 - */ "description": string; }; "_urlPreview": { - /** - * URLプレビューのサムネイルを非表示 - */ "title": string; - /** - * URLプレビューのサムネイル画像が読み込まれなくなります。 - */ "description": string; }; "_code": { - /** - * コードハイライトを非表示 - */ "title": string; - /** - * MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。 - */ "description": string; }; }; - "_hemisphere": { - /** - * 北半球 - */ - "N": string; - /** - * 南半球 - */ - "S": string; - /** - * 一部のクライアント設定で、季節を判定するために使用します。 - */ - "caption": string; - }; - "_reversi": { - /** - * リバーシ - */ - "reversi": string; - /** - * 対局の設定 - */ - "gameSettings": string; - /** - * ボードを選択 - */ - "chooseBoard": string; - /** - * 先行/後攻 - */ - "blackOrWhite": string; - /** - * {name}が黒(先行) - */ - "blackIs": ParameterizedString<"name">; - /** - * ルール - */ - "rules": string; - /** - * 対局はまもなく開始されます - */ - "thisGameIsStartedSoon": string; - /** - * 相手の準備が完了するのを待っています - */ - "waitingForOther": string; - /** - * あなたの準備が完了するのを待っています - */ - "waitingForMe": string; - /** - * 準備してください - */ - "waitingBoth": string; - /** - * 準備完了 - */ - "ready": string; - /** - * 準備を再開 - */ - "cancelReady": string; - /** - * 相手のターンです - */ - "opponentTurn": string; - /** - * あなたのターンです - */ - "myTurn": string; - /** - * {name}のターンです - */ - "turnOf": ParameterizedString<"name">; - /** - * {name}のターン - */ - "pastTurnOf": ParameterizedString<"name">; - /** - * 投了 - */ - "surrender": string; - /** - * 投了により - */ - "surrendered": string; - /** - * 時間切れ - */ - "timeout": string; - /** - * 引き分け - */ - "drawn": string; - /** - * {name}の勝ち - */ - "won": ParameterizedString<"name">; - /** - * 黒 - */ - "black": string; - /** - * 白 - */ - "white": string; - /** - * 合計 - */ - "total": string; - /** - * {count}ターン目 - */ - "turnCount": ParameterizedString<"count">; - /** - * 自分の対局 - */ - "myGames": string; - /** - * みんなの対局 - */ - "allGames": string; - /** - * 終了 - */ - "ended": string; - /** - * 対局中 - */ - "playing": string; - /** - * 石の少ない方が勝ち(ロセオ) - */ - "isLlotheo": string; - /** - * ループマップ - */ - "loopedMap": string; - /** - * どこでも置けるモード - */ - "canPutEverywhere": string; - /** - * 1ターンの時間制限 - */ - "timeLimitForEachTurn": string; - /** - * フリーマッチ - */ - "freeMatch": string; - /** - * 対戦相手を探しています - */ - "lookingForPlayer": string; - /** - * 対局がキャンセルされました - */ - "gameCanceled": string; - /** - * 開始時に対局をタイムラインに投稿 - */ - "shareToTlTheGameWhenStart": string; - /** - * 対局を開始しました! #MisskeyReversi - */ - "iStartedAGame": string; - /** - * 相手が設定を変更しました - */ - "opponentHasSettingsChanged": string; - /** - * 変則許可 (完全フリー) - */ - "allowIrregularRules": string; - /** - * 変則なし - */ - "disallowIrregularRules": string; - /** - * 盤面に行・列番号を表示 - */ - "showBoardLabels": string; - /** - * 石をアイコンにする - */ - "useAvatarAsStone": string; - }; - "_offlineScreen": { - /** - * オフライン - サーバーに接続できません - */ - "title": string; - /** - * サーバーに接続できません - */ - "header": string; - }; - "_urlPreviewSetting": { - /** - * URLプレビューの設定 - */ - "title": string; - /** - * URLプレビューを有効にする - */ - "enable": string; - /** - * プレビュー取得時のタイムアウト(ms) - */ - "timeout": string; - /** - * プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。 - */ - "timeoutDescription": string; - /** - * Content-Lengthの最大値(byte) - */ - "maximumContentLength": string; - /** - * Content-Lengthがこの値を超えた場合、プレビューは生成されません。 - */ - "maximumContentLengthDescription": string; - /** - * Content-Lengthが取得できた場合のみプレビューを生成 - */ - "requireContentLength": string; - /** - * 相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。 - */ - "requireContentLengthDescription": string; - /** - * User-Agent - */ - "userAgent": string; - /** - * プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。 - */ - "userAgentDescription": string; - /** - * プレビューを生成するプロキシのエンドポイント - */ - "summaryProxy": string; - /** - * CherryPick本体ではなく、サマリープロキシを使用してプレビューを生成します。 - */ - "summaryProxyDescription": string; - /** - * プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。 - */ - "summaryProxyDescription2": string; - }; - "_mediaControls": { - /** - * ピクチャインピクチャ - */ - "pip": string; - /** - * 再生速度 - */ - "playbackRate": string; - /** - * ループ再生 - */ - "loop": string; - }; - "_contextMenu": { - /** - * コンテキストメニュー - */ - "title": string; - /** - * アプリケーション - */ - "app": string; - /** - * Shiftキーでアプリケーション - */ - "appWithShift": string; - /** - * ブラウザのUI - */ - "native": string; - }; "_abuse": { "_resolver": { - /** - * 一時間 - */ "1hour": string; - /** - * 半日 - */ "12hours": string; - /** - * 一日 - */ "1day": string; - /** - * 一週間 - */ "1week": string; - /** - * 一ヶ月 - */ "1month": string; - /** - * 三ヶ月 - */ "3months": string; - /** - * 六ヶ月 - */ "6months": string; - /** - * 一年 - */ "1year": string; - /** - * 無期限 - */ "indefinitely": string; - /** - * この条件の有効期限 - */ "expiresAt": string; - /** - * 通報先のパターン - */ "targetUserPattern": string; - /** - * 通報元のパターン - */ "reporterPattern": string; - /** - * 通報内容のパターン - */ "reportContentPattern": string; }; - /** - * 一覧 - */ "list": string; - /** - * リソルバー - */ "resolver": string; }; "_imageCompressionMode": { - /** - * 画像の圧縮形式 - */ "title": string; - /** - * オリジナル画像を保持しない場合に、Web公開用画像の圧縮形式を選択できます。縮小する場合は2048x2048より小さくなるように縮小されます。非可逆圧縮を指定しない場合は、元画像に応じて非可逆圧縮か可逆圧縮かが自動的に選択されます。 - */ "description": string; - /** - * 縮小して再圧縮する - */ "resizeCompress": string; - /** - * 縮小せず再圧縮する - */ "noResizeCompress": string; - /** - * 縮小して非可逆圧縮する - */ "resizeCompressLossy": string; - /** - * 縮小せず非可逆圧縮する - */ "noResizeCompressLossy": string; }; - "_advancedSearch": { - "_fileOption": { - /** - * ファイルの添付状態 - */ - "title": string; - /** - * あり - */ - "fileAttachedOnly": string; - /** - * なし - */ - "noFile": string; - /** - * 全て - */ - "combined": string; - }; - "_searchOption": { - /** - * CW付きを除外する - */ - "toggleCW": string; - /** - * リプライを除外する - */ - "toggleReply": string; - /** - * 日時を指定する - */ - "toggleDate": string; - /** - * 高度な検索を有効にする - */ - "toggleAdvancedSearch": string; - /** - * 引用を除外する - */ - "toggleQuote": string; - }; - "_specifyDate": { - /** - * から - */ - "startDate": string; - /** - * まで - */ - "endDate": string; - }; - "_description": { - /** - * その他の設定 - */ - "other": string; - }; - "_fileNsfwOption": { - /** - * 添付ファイルのセンシティブ状態 - */ - "title": string; - /** - * フィルタしない - */ - "combined": string; - /** - * 除外 - */ - "withOutSensitive": string; - /** - * 含むもの - */ - "includeSensitive": string; - /** - * 全てセンシティブ - */ - "sensitiveOnly": string; - }; - }; - "_searchOrApShow": { - /** - * 照会を行いますか? - */ - "question": string; - /** - * 検索 - */ - "search": string; - /** - * 照会 - */ - "lookup": string; - }; - "_dice": { - /** - * サイコロを振る - */ - "rollDice": string; - /** - * サイコロの数 - */ - "diceCount": string; - /** - * サイコロの面数 - */ - "diceFaces": string; - }; - "_isIndexable": { - /** - * 公開ノートをインデックス化 - */ - "title": string; - /** - * kmy互換機能。公開ノートをインデックス化するかどうかを設定します。 - */ - "description": string; - }; - "_altWarning": { - /** - * ファイルに代替テキストが設定されていません。 - */ - "noAltWarning": string; - /** - * 画像に代替テキストが設定されていない場合に警告を表示する - */ - "showNoAltWarning": string; - /** - * 投稿フォームへ - */ - "postAnyWay": string; - }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/index.js b/locales/index.js index 8bbe27694c..3227ee6ac5 100644 --- a/locales/index.js +++ b/locales/index.js @@ -53,11 +53,7 @@ const primaries = { const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); export function build() { - // vitestの挙動を調整するため、一度ローカル変数化する必要がある - // https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577 - // https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785 - const metaUrl = import.meta.url; - const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {}); + const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す const removeEmpty = (obj) => { diff --git a/locales/it-IT.yml b/locales/it-IT.yml index a80b32c88e..8e7afc891c 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -85,7 +85,7 @@ note: "Nota" notes: "Note" following: "Follow" followers: "Follower" -followsYou: "Follower" +followsYou: "Segue" createList: "Aggiungi una nuova lista" manageLists: "Gestisci liste" error: "Errore" @@ -102,20 +102,17 @@ defaultNoteVisibility: "Privacy predefinita delle note" follow: "Segui" followRequest: "Richiesta di follow" followRequests: "Richieste di follow" -unfollow: "Smetti di seguire" +unfollow: "Interrompi following" followRequestPending: "Richiesta in approvazione" enterEmoji: "Inserisci emoji" renote: "Rinota" unrenote: "Elimina la Rinota" renoted: "Rinotata!" -renotedToX: "Rinota da {name}." cantRenote: "È impossibile rinotare questa nota." cantReRenote: "È impossibile rinotare una Rinota." quote: "Citazione" inChannelRenote: "Rinota nel canale" inChannelQuote: "Cita nel canale" -renoteToChannel: "Rinota al canale" -renoteToOtherChannel: "Rinota a un altro canale" pinnedNote: "Nota in primo piano" pinned: "Fissa sul profilo" you: "Tu" @@ -133,16 +130,15 @@ overwriteFromPinnedEmojis: "Sovrascrivi con le impostazioni globali" reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere." rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note" attachCancel: "Rimuovi allegato" -deleteFile: "File da Drive eliminato" markAsSensitive: "Segna come esplicito" unmarkAsSensitive: "Non segnare come esplicito " enterFileName: "Nome del file" -mute: "Silenziare" +mute: "Silenzia" unmute: "Riattiva l'audio" -renoteMute: "Silenziare le Rinota" +renoteMute: "Silenzia le Rinota" renoteUnmute: "Non silenziare le Rinota" -block: "Bloccare" -unblock: "Sbloccare" +block: "Blocca" +unblock: "Sblocca" suspend: "Sospensione" unsuspend: "Revoca la sospensione" blockConfirm: "Vuoi davvero bloccare il profilo?" @@ -180,10 +176,6 @@ addAccount: "Aggiungi profilo" reloadAccountsList: "Ricarica l'elenco dei profili" loginFailed: "Accesso non riuscito" showOnRemote: "Leggi sull'istanza remota" -continueOnRemote: "Continua da remoto" -chooseServerOnMisskeyHub: "Scegli l'istanza sul sito Misskey Hub" -specifyServerHost: "Indica l'indirizzo dell'istanza" -inputHostName: "Digita il nome del dominio " general: "Generali" wallpaper: "Sfondo" setWallpaper: "Imposta sfondo" @@ -207,8 +199,8 @@ charts: "Grafici" perHour: "orario" perDay: "giornaliero" stopActivityDelivery: "Interrompi la distribuzione di attività" -blockThisInstance: "Bloccare l'istanza" -silenceThisInstance: "Silenziare l'istanza" +blockThisInstance: "Blocca questa istanza" +silenceThisInstance: "Silenzia l'istanza" operations: "Operazioni" software: "Software" version: "Versione" @@ -230,7 +222,7 @@ blockedInstances: "Istanze bloccate" blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga. Esse non potranno più interagire con la tua istanza." silencedInstances: "Istanze silenziate" silencedInstancesDescription: "Elenca i nomi host delle istanze che vuoi silenziare. Tutti i profili nelle istanze silenziate vengono trattati come tali. Possono solo inviare richieste di follow e menzionare soltanto i profili locali che seguono. Le istanze bloccate non sono interessate." -muteAndBlock: "Silenziare e bloccare" +muteAndBlock: "Silenziati / Bloccati" mutedUsers: "Profili silenziati" blockedUsers: "Profili bloccati" noUsers: "Non ci sono profili" @@ -320,7 +312,6 @@ selectFile: "Scelta allegato" selectFiles: "Scelta allegato" selectFolder: "Seleziona cartella" selectFolders: "Seleziona cartella" -fileNotSelected: "Nessun file selezionato" renameFile: "Rinomina file" folderName: "Nome della cartella" createFolder: "Nuova cartella" @@ -388,11 +379,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Abilita hCaptcha" hcaptchaSiteKey: "Chiave del sito" hcaptchaSecretKey: "Chiave segreta" -mcaptcha: "mCaptcha" -enableMcaptcha: "Abilita hCaptcha" -mcaptchaSiteKey: "Chiave del sito" -mcaptchaSecretKey: "Chiave segreta" -mcaptchaInstanceUrl: "URL della istanza mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Abilita reCAPTCHA" recaptchaSiteKey: "Chiave del sito" @@ -408,7 +394,6 @@ name: "Nome" antennaSource: "Fonte dell'antenna" antennaKeywords: "Parole chiavi da ricevere" antennaExcludeKeywords: "Parole chiavi da escludere" -antennaExcludeBots: "Escludere i Bot" antennaKeywordsDescription: "Sparando con uno spazio indichi la condizione E (and). Separando con un a capo, indichi la condizione O (or)." notifyAntenna: "Invia notifiche delle nuove note" withFileAntenna: "Solo note con file in allegato" @@ -419,7 +404,7 @@ withReplies: "Includere le risposte" connectedTo: "Connessione ai seguenti profili:" notesAndReplies: "Note e risposte" withFiles: "Con allegati" -silence: "Silenziare" +silence: "Silenzia" silenceConfirm: "Vuoi davvero silenziare questo profilo?" unsilence: "Riattiva" unsilenceConfirm: "Vuoi davvero riattivare questo profilo?" @@ -444,7 +429,7 @@ moderation: "moderazione" moderationNote: "Promemoria di moderazione" addModerationNote: "Aggiungi promemoria di moderazione" moderationLogs: "Cronologia di moderazione" -nUsersMentioned: "{n} profili ne parlano" +nUsersMentioned: "{n} profili menzionati" securityKeyAndPasskey: "Chiave di sicurezza e accesso" securityKey: "Chiave di sicurezza" lastUsed: "Ultima attività" @@ -459,7 +444,7 @@ share: "Condividi" notFound: "Non trovato" notFoundDescription: "Nessuna pagina corrisponde all'URL indicata." uploadFolder: "Destinazione caricamento predefinita" -markAsReadAllNotifications: "Segnare tutte le notifiche come lette" +markAsReadAllNotifications: "Segna tutte le notifiche come lette" markAsReadAllUnreadNotes: "Segna tutte le note come lette" markAsReadAllTalkMessages: "Segna tutte le chat come lette" help: "Guida" @@ -485,12 +470,10 @@ noteOf: "Note di {user}" inviteToGroup: "Invitare al gruppo" quoteAttached: "Citazione allegata" quoteQuestion: "Vuoi aggiungere una citazione?" -attachAsFileQuestion: "Il testo copiato eccede le dimensioni, vuoi allegarlo?" noMessagesYet: "Ancora nessuna chat" newMessageExists: "Hai ricevuto un nuovo messaggio" onlyOneFileCanBeAttached: "È possibile allegare al messaggio soltanto uno file" signinRequired: "Occorre avere un profilo registrato su questa istanza" -signinOrContinueOnRemote: "Per continuare, devi accedere alla tua istanza o registrarti su questa e poi accedere" invitations: "Invita" invitationCode: "Codice di invito" checking: "Confermando" @@ -517,7 +500,6 @@ disableDrawer: "Non mostrare il menù sul drawer" youHaveNoGroups: "Nessun gruppo" joinOrCreateGroup: "Puoi creare il tuo gruppo o essere invitat@ a gruppi che già esistono." showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mouse" -showReactionsCount: "Visualizza il numero di reazioni su una nota" noHistory: "Nessuna cronologia" signinHistory: "Storico degli accessi al profilo" enableAdvancedMfm: "Attiva MFM avanzati" @@ -601,7 +583,7 @@ scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScr output: "Uscita" script: "Script" disablePagesScript: "Disabilita AiScript nelle pagine" -updateRemoteUser: "Aggiorna dati dal profilo remoto" +updateRemoteUser: "Aggiorna le informazioni dal profilo remoto" unsetUserAvatar: "Rimozione foto profilo" unsetUserAvatarConfirm: "Vuoi davvero rimuovere la foto profilo?" unsetUserBanner: "Rimuovi intestazione profilo" @@ -611,7 +593,7 @@ deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?" removeAllFollowing: "Annulla tutti i follow" removeAllFollowingDescription: "Cancella tutti i follows del server {host}. Per favore, esegui se, ad esempio, l'istanza non esiste più." userSuspended: "L'utente è in sospensione" -userSilenced: "Profilo silenziato" +userSilenced: "Profilo silente." yourAccountSuspendedTitle: "Questo profilo è sospeso" yourAccountSuspendedDescription: "Questo profilo è stato sospeso a causa di una violazione del regolamento. Per informazioni, contattare l'amministrazione. Si prega di non creare un nuovo account." tokenRevoked: "Il token non è valido" @@ -656,7 +638,6 @@ medium: "Medio" small: "Piccolo" generateAccessToken: "Genera token di accesso" permission: "Autorizzazioni " -adminPermission: "Privilegi amministrativi" enableAll: "Abilita tutto" disableAll: "Disabilita tutto" tokenRequested: "Autorizza accesso al profilo" @@ -681,8 +662,8 @@ wordMute: "Filtri parole" hardWordMute: "Filtro parole forte" 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 parlato" +instanceMute: "Silenzia l'istanza" +userSaysSomething: "{name} ha detto qualcosa" makeActive: "Attiva" display: "Visualizza" copy: "Copia" @@ -700,24 +681,23 @@ useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifi other: "Ulteriori" regenerateLoginToken: "Genera di nuovo un token di connessione" regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi." -theKeywordWhenSearchingForCustomEmoji: "Questa sarà la parola chiave durante la ricerca di emoji personalizzate" setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi." fileIdOrUrl: "ID o URL del file" behavior: "Comportamento" sample: "Esempio" abuseReports: "Segnalazioni" -reportAbuse: "Segnalare" -reportAbuseRenote: "Segnalare la Rinota" -reportAbuseOf: "Segnalare {name}" +reportAbuse: "Segnala" +reportAbuseRenote: "Segnala la Rinota" +reportAbuseOf: "Segnala {name}" fillAbuseReportDescription: "Per favore, spiegaci il motivo della segnalazione. Se riguarda una Nota precisa, indica anche l'indirizzo URL." abuseReported: "La segnalazione è stata inviata. Grazie." reporter: "il corrispondente" -reporteeOrigin: "Segnalazione a" -reporterOrigin: "Segnalazione da" +reporteeOrigin: "Origine del segnalato" +reporterOrigin: "Origine del segnalatore" forwardReport: "Inoltro di un report a un'istanza remota." forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo." send: "Inviare" -abuseMarkAsResolved: "Risolvi segnalazione" +abuseMarkAsResolved: "Contrassegna la segnalazione come risolta" openInNewTab: "Apri in una nuova scheda" openInSideView: "Apri in vista laterale" defaultNavigationBehaviour: "Navigazione preimpostata" @@ -786,7 +766,7 @@ reloadToApplySetting: "Le tue preferenze verranno impostate dopo il ricaricament needReloadToApply: "È necessario riavviare per rendere effettive le modifiche." showTitlebar: "Visualizza la barra del titolo" clearCache: "Svuota la cache" -onlineUsersCount: "{n} persone attive adesso" +onlineUsersCount: "{n} persone online" nUsers: "{n} profili" nNotes: "{n}Note" sendErrorReports: "Invia segnalazioni di errori" @@ -854,7 +834,6 @@ administration: "Gestione" accounts: "Profilo" switch: "Cambia" noMaintainerInformationWarning: "Mancano le informazioni sull'amministratore." -noInquiryUrlWarning: "Non è stata impostata la URL di contatto" noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot" configure: "Imposta" postToGallery: "Pubblicare nella galleria" @@ -886,7 +865,7 @@ troubleshooting: "Risoluzione problemi" useBlurEffect: "Utilizza effetto sfocatura" learnMore: "Più dettagli" misskeyUpdated: "CherryPick è stato aggiornato!" -whatIsNew: "Informazioni sull'aggiornamento" +whatIsNew: "Visualizza le informazioni sull'aggiornamento" translate: "Traduci" translatedFrom: "Traduzione da {x}" accountDeletionInProgress: "È in corso l'eliminazione del profilo" @@ -898,7 +877,7 @@ pubSub: "Publish/Subscribe del profilo" lastCommunication: "La comunicazione più recente" resolved: "Risolto" unresolved: "Non risolto" -breakFollow: "Impedire di seguirmi" +breakFollow: "Interrompi follow" breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?" itsOn: "Abilitato" itsOff: "Disabilitato" @@ -912,10 +891,8 @@ manageAccounts: "Gestisci i profili" makeReactionsPublic: "Pubblicare la lista delle reazioni." makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a disposizione di tutti." classic: "Classico" -muteThread: "Silenziare conversazione" +muteThread: "Silenzia conversazione" unmuteThread: "Riattiva la conversazione" -followingVisibility: "Visibilità dei profili seguiti" -followersVisibility: "Visibilità dei profili che ti seguono" continueThread: "Altre conversazioni" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" incorrectPassword: "La password è errata." @@ -996,11 +973,11 @@ shuffle: "Casuale" account: "Account" move: "Sposta" pushNotification: "Notifiche Push" -subscribePushNotification: "Attivare le notifiche push" -unsubscribePushNotification: "Disattivare le notifiche push" +subscribePushNotification: "Attiva le notifiche push" +unsubscribePushNotification: "Disattiva le notifiche push" pushNotificationAlreadySubscribed: "Le notifiche push sono già attivate" pushNotificationNotSupported: "Il client o il server non supporta le notifiche push" -sendPushNotificationReadMessage: "Eliminare le notifiche push dopo la relativa lettura" +sendPushNotificationReadMessage: "Elimina le notifiche push dopo la relativa lettura" sendPushNotificationReadMessageCaption: "Se possibile, verrà mostrata brevemente una notifica con il testo \"{emptyPushNotificationMessage}\". Potrebbe influire negativamente sulla durata della batteria." windowMaximize: "Ingrandisci" windowMinimize: "Contrai finestra" @@ -1018,7 +995,6 @@ neverShow: "Non mostrare più" remindMeLater: "Rimanda" didYouLikeMisskey: "Ti piace CherryPick?" pleaseDonate: "CherryPick è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!" -correspondingSourceIsAvailable: "" roles: "Ruoli" role: "Ruolo" noRole: "Ruolo non trovato" @@ -1046,7 +1022,6 @@ thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale" thisPostMayBeAnnoyingCancel: "Annulla" thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso" collapseRenotes: "Comprimi le Rinota già viste" -collapseRenotesDescription: "Comprimi le Note con cui hai già interagito." internalServerError: "Errore interno del server" internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server" copyErrorInfo: "Copia le informazioni sull'errore" @@ -1070,9 +1045,6 @@ resetPasswordConfirm: "Vuoi davvero ripristinare la password?" sensitiveWords: "Parole esplicite" sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga." sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." -prohibitedWords: "Parole proibite" -prohibitedWordsDescription: "Verrà impedito di pubblicare Note che abbiano le parole indicate. Puoi impostare più parole, separatamente, su ogni riga." -prohibitedWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." hiddenTags: "Hashtag nascosti" hiddenTagsDescription: "Impedire la visualizzazione del tag impostato nei trend. Puoi impostare più valori, uno per riga." notesSearchNotAvailable: "Non è possibile cercare tra le Note." @@ -1091,8 +1063,6 @@ limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale" noteIdOrUrl: "ID della Nota o URL" video: "Video" videos: "Video" -audio: "Audio" -audioFiles: "Audio" dataSaver: "Risparmia dati" accountMigration: "Migrazione del profilo" accountMoved: "Questo profilo ha migrato altrove:" @@ -1188,7 +1158,6 @@ showRenotes: "Includi le Rinota" edited: "Modificato" notificationRecieveConfig: "Preferenze di notifica" mutualFollow: "Follow reciproco" -followingOrFollower: "Following o Follower" fileAttachedOnly: "Solo con allegati" showRepliesToOthersInTimeline: "Risposte altrui nella TL" hideRepliesToOthersInTimeline: "Nascondi Riposte altrui nella TL" @@ -1197,13 +1166,6 @@ hideRepliesToOthersInTimelineAll: "Nascondi le risposte dei tuoi follow nella TL confirmShowRepliesAll: "Questa è una attività irreversibile. Vuoi davvero includere tutte le risposte dei following in TL?" confirmHideRepliesAll: "Questa è una attività irreversibile. Vuoi davvero escludere tutte le risposte dei following in TL?" externalServices: "Servizi esterni" -sourceCode: "Codice sorgente" -sourceCodeIsNotYetProvided: "" -repositoryUrl: "URL della repository" -repositoryUrlDescription: "Se esiste un repository il cui il codice sorgente è disponibile pubblicamente, inserisci il suo URL. Se stai utilizzando CherryPick così com'è (senza alcuna modifica al codice sorgente), inserisci https://github.com/kokonect-link/cherrypick." -repositoryUrlOrTarballRequired: "Se non disponi di un repository pubblico, dovrai fornire un file tarball (tar). Vedere .config/example.yml per i dettagli." -feedback: "Feedback" -feedbackUrl: "URL di feedback" impressum: "Dichiarazione di proprietà" impressumUrl: "URL della dichiarazione di proprietà" impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)." @@ -1231,63 +1193,6 @@ remainingN: "Rimangono: {n}" overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?" seasonalScreenEffect: "Schermate in base alla stagione" decorate: "Decora" -addMfmFunction: "Aggiungi decorazioni" -enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM" -bubbleGame: "Bubble Game" -sfx: "Effetti sonori" -soundWillBePlayed: "Con musica ed effetti sonori" -showReplay: "Vedi i replay" -replay: "Replay" -replaying: "Replay in corso" -endReplay: "Termina replay" -copyReplayData: "Copia replay" -ranking: "Classifica" -lastNDays: "Ultimi {n} giorni" -backToTitle: "Torna al titolo" -hemisphere: "Geolocalizzazione" -withSensitive: "Mostra le Note con allegati espliciti" -userSaysSomethingSensitive: "Note da {name} con allegati espliciti" -enableHorizontalSwipe: "Trascina per invertire i tab" -loading: "Caricamento" -surrender: "Annulla" -gameRetry: "Riprova" -notUsePleaseLeaveBlank: "Lasciare vuoto, se non in uso" -useTotp: "Usare il codice OTP" -useBackupCode: "Usare il codice usa-e-getta" -launchApp: "Esegui l'App" -useNativeUIForVideoAudioPlayer: "Riprodurre audio/video usando le funzionalità del browser" -keepOriginalFilename: "Mantieni il nome file originale" -keepOriginalFilenameDescription: "Disattivandola, i file verranno caricati usando nomi casuali." -noDescription: "Manca la descrizione" -alwaysConfirmFollow: "Richiedi conferma per i Follow" -inquiry: "Contattaci" -tryAgain: "Per favore riprova" -confirmWhenRevealingSensitiveMedia: "Richiedi conferma prima di mostrare gli allegati espliciti" -sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?" -_delivery: - status: "Stato della consegna" - stop: "Sospensione" - resume: "Riprendi la consegna" - _type: - none: "Pubblicazione" - manuallySuspended: "Sospesa manualmente" - goneSuspended: "Sospensione server a causa dell'eliminazione" - autoSuspendedForNotResponding: "Sospensione del server a causa di mancata risposta" -_bubbleGame: - howToPlay: "Come giocare" - hold: "Tieni" - _score: - score: "Punteggio" - scoreYen: "Capitale" - highScore: "Punteggio migliore" - maxChain: "Miglior combo" - yen: "{yen}¥" - estimatedQty: "{qty} punti" - scoreSweets: "Onigiri {onigiriQtyWithUnit}" - _howToPlay: - section1: "Scegli la posizione e rilascia l'oggetto nel contenitore." - section2: "Se due oggetti dello stesso tipo si toccano, si trasformano in un oggetto diverso, aumentando il punteggio." - section3: "Se gli oggetti escono dal limite superiore del contenitore, il gioco finisce. Cerca di ottenere un punteggio elevato fondendo gli oggetti, evitando che escano dal contenitore!" _announcement: forExistingUsers: "Solo ai profili attuali" forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." @@ -1299,7 +1204,7 @@ _announcement: readConfirmText: "Hai già letto \"{title}˝?" shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte." dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte." - silence: "Silenziare gli annunci" + silence: "Silenzia gli annunci" silenceDescription: "Se attivi questa opzione, non riceverai notifiche sugli annunci, evitando di contrassegnarle come già lette." _initialAccountSetting: accountCreated: "Il tuo profilo è stato creato!" @@ -1338,14 +1243,14 @@ _initialTutorial: letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"+\" (più) della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!" reactToContinue: "Aggiungere la Reazione ti consentirà di procedere col tutorial." reactNotification: "Quando qualcuno reagisce alle tue Note, ricevi una notifica in tempo reale." - reactDone: "Annulla la tua Reazione premendo il bottone \"ー\" (meno)" + reactDone: "Puoi annullare la tua Reazione premendo il bottone \"ー\" (meno)" _timeline: title: "Come funziona la Timeline" description1: "CherryPick fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori." - home: "le Note provenienti dai profili che segui (follow)." - local: "tutte le Note pubblicate dai profili di questa istanza." - social: "sia le Note della Timeline Home che quelle della Timeline Locale, insieme!" - global: "le Note da pubblicate da tutte le altre istanze federate con la nostra." + home: "Puoi vedere le Note provenienti dai profili che segui (follow)." + local: "Puoi vedere tutte le Note pubblicate dai profili di questa istanza." + social: "Puoi vedere sia le Note della Timeline Home che quelle della Timeline Locale, insieme!" + global: "Puoi vedere le Note da pubblicate da tutte le altre istanze federate con la nostra." description2: "Nella parte superiore dello schermo, puoi scegliere una Timeline o l'altra in qualsiasi momento." description3: "Ci sono anche sequenze temporali di elenchi, sequenze temporali di canali, ecc. Per ulteriori dettagli, consultare il {link}.\nPuoi vedere anche Timeline delle liste di profili (se ne hai create), canali, ecc... Per i dettagli, visita {link}." _postNote: @@ -1369,13 +1274,13 @@ _initialTutorial: useCases: "Utilizzalo per chiarire il contenuto della Nota, prima che sia letta. Come richiesto dal regolamento del server o per autoregolamentare spoiler e testi troppo espliciti." _howToMakeAttachmentsSensitive: title: "Come indicare che gli allegati sono espliciti?" - description: "Si fa quando è richiesto dal regolamento del server o quando non devono essere visibili immediatamente." + description: "Contrassegnare gli allegati come espliciti, va fatto quando è richiesto dal regolamento del server o quando gli allegati non devono essere immediatamente visibili." tryThisFile: "Prova a rendere esplicite le immagini allegate a questo modulo!" _exampleNote: - note: "AAA! Ho rotto il coperchio del natto... (fagioli di soia fermentati)" - method: "Tocca il file, si aprirà il menu, scegli la voce \"Segna come esplicito\"" - sensitiveSucceeded: "Quando alleghi file, assicurati di indicare se è materiale esplicito in modo appropriato, decidi in base al regolamento dell'istanza." - doItToContinue: "Imposta l'immagine come esplicita per procedere col tutorial." + note: "Ho fatto un errore aprendo il coperchio del natto... (fagioli di soia fermentati, particolarmente appiccicosi)" + method: "Per indicare che un allegato è esplicito, tocca il file per aprirne il menu e scegliere la voce \"Segna come esplicito\"." + sensitiveSucceeded: "Quando alleghi file, assicurati di indicare se è materiale esplicito, in modo appropriato, in base al regolamento del tuo server." + doItToContinue: "Impostando l'immagine come esplicita, potrai procedere col tutorial." _done: title: "Il tutorial è finito! 🎉" description: "Queste sono solamente alcune delle funzionalità principali di CherryPick. Per ulteriori informazioni, {link}." @@ -1398,12 +1303,10 @@ _serverSettings: fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare." fanoutTimelineDbFallback: "Elaborazione dati alternativa" fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline." - inquiryUrl: "URL di contatto" - inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione." _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" - moveFromLabel: "Profilo da cui migrare #{n}" + moveFromLabel: "Profilo da cui migrare:" moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it" moveTo: "Migrare questo profilo verso un un altro" moveToLabel: "Profilo verso cui migrare" @@ -1471,7 +1374,7 @@ _achievements: _login3: title: "Principiante I" description: "Hai totalizzato 3 accessi!" - flavor: "Da oggi, chiamatemi Cherrypikist" + flavor: "Da oggi, chiamatemi Misskist" _login7: title: "Principiante II" description: "Hai totalizzato 7 accessi!" @@ -1663,13 +1566,6 @@ _achievements: _tutorialCompleted: title: "Attestato di partecipazione al corso per principianti di CherryPick" description: "Ha completato il tutorial" - _bubbleGameExplodingHead: - title: "🤯" - description: "Estrai l'oggetto più grande dal Bubble Game" - _bubbleGameDoubleExplodingHead: - title: "Doppio 🤯" - description: "Due oggetti più grossi contemporaneamente nel Bubble Game" - flavor: "Ha le dimensioni di una bento-box 🤯 🤯" _role: new: "Nuovo ruolo" edit: "Modifica ruolo" @@ -1710,7 +1606,6 @@ _role: gtlAvailable: "Disponibilità della Timeline Federata" ltlAvailable: "Disponibilità della Timeline Locale" canPublicNote: "Scrivere Note con Visibilità Pubblica" - mentionMax: "Numero massimo di menzioni in una nota" canInvite: "Generare codici di invito all'istanza" inviteLimit: "Limite di codici invito" inviteLimitCycle: "Intervallo di emissione del codice di invito" @@ -1719,7 +1614,6 @@ _role: canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo" driveCapacity: "Capienza del Drive" alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)" - canUpdateBioMedia: "Può aggiornare foto profilo e di testata" pinMax: "Quantità massima di Note in primo piano" antennaMax: "Quantità massima di Antenne" wordMuteMax: "Lunghezza massima del filtro parole" @@ -1735,14 +1629,8 @@ _role: canUseTranslator: "Tradurre le Note" avatarDecorationLimit: "Numero massimo di decorazioni foto profilo installabili" _condition: - roleAssignedTo: "Assegnato a ruoli manualmente" isLocal: "Profilo locale" isRemote: "Profilo remoto" - isCat: "È un gattino" - isBot: "È un bot" - isSuspended: "È sospeso" - isLocked: "È in stato privato" - isExplorable: "Autorizza la pubblicazione nei cataloghi" createdLessThan: "Profilo creato da meno di N" createdMoreThan: "Profilo creato da più di N" followersLessThanOrEq: "Profilo con N follower o meno" @@ -1768,7 +1656,6 @@ _emailUnavailable: disposable: "Indirizzo email non utilizzabile" mx: "Server email non corretto" smtp: "Il server email non risponde" - banned: "Non puoi registrarti con questo indirizzo email" _ffVisibility: public: "Pubblica" followers: "Mostra solo ai follower" @@ -1812,7 +1699,6 @@ _plugin: installWarn: "Si prega di installare soltanto estensioni che provengono da fonti affidabili." manage: "Gestisci estensioni" viewSource: "Visualizza sorgente" - viewLog: "Mostra log" _preferencesBackups: list: "Elenco di impostazioni salvate in precedenza" saveNew: "Nuovo salvataggio" @@ -1842,8 +1728,6 @@ _aboutMisskey: contributors: "Principali sostenitori" allContributors: "Tutti i sostenitori" source: "Codice sorgente" - original: "Originale" - thisIsModifiedVersion: "{name} sta usando una versione modificata diversa da CherryPick originale." translation: "Tradurre Misskey" donate: "Sostieni Misskey" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" @@ -1953,7 +1837,7 @@ _instanceMute: instanceMuteDescription: "Disattiva tutte le note, le note di rinvio (condivisione) dell'istanza configurata, comprese le risposte agli utenti dell'istanza." instanceMuteDescription2: "Impostazione separata da una nuova riga" title: "Nasconde le note dell'istanza configurata." - heading: "Istanze da silenziare" + heading: "Istanze da silenziare." _theme: explore: "Esplora temi" install: "Installa un tema" @@ -2031,6 +1915,8 @@ _sfx: notification: "Notifiche" chat: "Messaggi" chatBg: "Chat (sfondo)" + antenna: "Ricezione dell'antenna" + channel: "Notifiche di canale" reaction: "Quando seleziono una reazione" _soundSettings: driveFile: "Suoni del Drive" @@ -2068,6 +1954,7 @@ _2fa: registerTOTP: "Registra una App di autenticazione a due fattori (2FA/MFA)" step1: "Innanzitutto, installa sul dispositivo un'App di autenticazione come {a} o {b}." step2: "Quindi, tramite la App installata, scansiona questo codice QR." + step2Click: "Cliccando sul codice QR, puoi registrarlo con l'app di autenticazione o il portachiavi installato sul tuo dispositivo." step2Uri: "Inserisci il seguente URL se desideri utilizzare una App per PC" step3Title: "Inserisci il codice di verifica" step3: "Inserite il token visualizzato nell'app e il gioco è fatto." @@ -2091,7 +1978,6 @@ _2fa: backupCodesDescription: "Puoi usare questi codici usa-e-getta per ottenere l'accesso al tuo profilo in caso sia impossibile usare l'App col codice OTP. Salvali in un posto sicuro." backupCodeUsedWarning: "È stato usato un codice usa-e-getta. Per favore, riconfigura l'autenticazione a due fattori il prima possibile, nel caso la configurazione precedente abbia smesso di funzionare." backupCodesExhaustedWarning: "Hai esaurito i codici usa-e-getta. Se l'App che genera il codice OTP non è più disponibile, non potrai più accedere al tuo profilo. Ripeti la configurazione per l'autenticazione a due fattori." - moreDetailedGuideHere: "Informazioni dettagliate sull'autenticazione multi fattore (2FA/MFA)" _permissions: "read:account": "Visualizza le informazioni sul profilo" "write:account": "Modifica le informazioni sul profilo" @@ -2108,8 +1994,8 @@ _permissions: "read:mutes": "Vedi i profili silenziati" "write:mutes": "Gestisci i profili silenziati" "write:notes": "Creare / Eliminare note" - "read:notifications": "Visualizzare notifiche" - "write:notifications": "Gestire notifiche" + "read:notifications": "Visualizza notifiche" + "write:notifications": "Gerisci notifiche" "read:reactions": "Vedi reazioni" "write:reactions": "Gerisci reazioni" "write:votes": "Votare" @@ -2129,54 +2015,6 @@ _permissions: "write:flash": "Modifica Play" "read:flash-likes": "Visualizza lista di Play piaciuti" "write:flash-likes": "Modifica lista di Play piaciuti" - "read:admin:abuse-user-reports": "Mostra i report dai profili utente" - "write:admin:delete-account": "Elimina l'account utente" - "write:admin:delete-all-files-of-a-user": "Elimina i file dell'account utente" - "read:admin:index-stats": "Visualizza informazioni sugli indici del database" - "read:admin:table-stats": "Visualizza informazioni sulle tabelle del database" - "read:admin:user-ips": "Visualizza indirizzi IP degli account" - "read:admin:meta": "Visualizza i metadati dell'istanza" - "write:admin:reset-password": "Ripristina la password dell'account utente" - "write:admin:resolve-abuse-user-report": "Risolvere le segnalazioni dagli account utente" - "write:admin:send-email": "Spedire email" - "read:admin:server-info": "Vedere le informazioni sul server" - "read:admin:show-moderation-log": "Vedere lo storico di moderazione" - "read:admin:show-user": "Vedere le informazioni private degli account utente" - "write:admin:suspend-user": "Sospendere i profili" - "write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili" - "write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili" - "write:admin:unsuspend-user": "Togliere la sospensione ai profili" - "write:admin:meta": "Modificare i metadati dell'istanza" - "write:admin:user-note": "Scrivere annotazioni di moderazione" - "write:admin:roles": "Gestire i ruoli" - "read:admin:roles": "Vedere i ruoli" - "write:admin:relays": "Gestire i Relay" - "read:admin:relays": "Vedere i Relay" - "write:admin:invite-codes": "Gestire codici di invito" - "read:admin:invite-codes": "Vedere codici di invito" - "write:admin:announcements": "Gestire gli annunci" - "read:admin:announcements": "Leggere gli annunci" - "write:admin:avatar-decorations": "Gestire le decorazioni" - "read:admin:avatar-decorations": "Vedere le decorazioni" - "write:admin:federation": "Gestire la federazione" - "write:admin:account": "Vedere la federazione" - "read:admin:account": "Vedere le utenze" - "write:admin:emoji": "Gestire le emoji personalizzate" - "read:admin:emoji": "Vedere le emoji personalizzate" - "write:admin:queue": "Gestire la coda di attività" - "read:admin:queue": "Vedere la coda di attività" - "write:admin:promo": "Gestire le promozioni" - "write:admin:drive": "Gestire il Drive degli account" - "read:admin:drive": "Vedere il Drive degli account" - "read:admin:stream": "Usare le API Websocket" - "write:admin:ad": "Gestire i banner pubblicitari" - "read:admin:ad": "Vedere i banner pubblicitari" - "write:invite-codes": "Creare codici di invito" - "read:invite-codes": "Vedere i codici di invito" - "write:clip-favorite": "Impostare Clip preferite" - "read:clip-favorite": "Vedere Clip preferite" - "read:federation": "Vedere la federazione" - "write:report-abuse": "Inviare segnalazioni" _auth: shareAccessTitle: "Permessi dell'applicazione" shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?" @@ -2222,7 +2060,7 @@ _widgets: postForm: "Finestra di pubblicazione" slideshow: "Diapositive" button: "Pulsante" - onlineUsers: "Persone attive adesso" + onlineUsers: "Persone online" jobQueue: "Coda di lavoro" serverMetric: "Statistiche server" aiscript: "Console AiScript" @@ -2299,7 +2137,6 @@ _profile: _exportOrImport: allNotes: "Tutte le note" favoritedNotes: "Note preferite" - clips: "Clip" followingList: "Follow" muteList: "Elenco profili silenziati" blockingList: "Elenco profili bloccati" @@ -2354,7 +2191,6 @@ _play: title: "Titolo" script: "Script" summary: "Descrizione" - visibilityDescription: "Impostarlo su privato significa che non verrà visualizzato sul tuo profilo, ma chiunque ha l'URL potrà comunque accedervi." _pages: newPage: "Crea pagina" editPage: "Modifica pagina" @@ -2399,8 +2235,6 @@ _pages: section: "Sezione" image: "Immagini" button: "Pulsante" - dynamic: "Riquadri dinamici" - dynamicDescription: "Questo riquadro è obsoleto. Utilizza {play} da ora in poi." note: "Nota integrata" _note: id: "ID nota" @@ -2423,18 +2257,15 @@ _notification: pollEnded: "Risultati del sondaggio." newNote: "Nuove Note" unreadAntennaNote: "Antenna {name}" - roleAssigned: "Ruolo assegnato" emptyPushNotificationMessage: "Le notifiche push sono state aggiornate." achievementEarned: "Obiettivo raggiunto" - testNotification: "Provare la notifica" - checkNotificationBehavior: "Provare il comportamento della notifica" + testNotification: "Prova la notifica" + checkNotificationBehavior: "Prova il comportamento della notifica" sendTestNotification: "Spedisci una notifica di prova" notificationWillBeDisplayedLikeThis: "La notifica apparirà così" reactedBySomeUsers: "{n} reazioni" - likedBySomeUsers: "{n} apprezzamenti" renotedBySomeUsers: "{n} Rinota" - followedBySomeUsers: "{n} follower" - flushNotification: "Azzera le notifiche" + followedBySomeUsers: "{n} nuovi follower" _types: all: "Tutto" note: "Nuove Note" @@ -2448,7 +2279,6 @@ _notification: receiveFollowRequest: "Richiesta di follow ricevuta" followRequestAccepted: "Richiesta di follow accettata" groupInvited: "Invito a un gruppo" - roleAssigned: "Ruolo concesso" achievementEarned: "Risultato raggiunto" app: "Notifiche da applicazioni" _actions: @@ -2459,7 +2289,6 @@ _deck: alwaysShowMainColumn: "Mostra sempre la colonna principale" columnAlign: "Allineare colonne" addColumn: "Aggiungi colonna" - newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note" configureColumn: "Impostazioni colonna" swapLeft: "Sposta a sinistra" swapRight: "Sposta a destra" @@ -2488,8 +2317,8 @@ _deck: direct: "Note Dirette" roleTimeline: "Timeline Ruolo" _dialog: - charactersExceeded: "Hai superato il limite di {max} caratteri! ({current})" - charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})" + charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})" + charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})" _disabledTimeline: title: "Timeline disabilitata" description: "Il ruolo in cui sei non ti permette di leggere questa timeline" @@ -2498,9 +2327,9 @@ _drivecleaner: orderByCreatedAtAsc: "Dal più vecchio al più recente" _webhookSettings: createWebhook: "Creazione Webhook" - modifyWebhook: "Modifica Webhook" name: "Nome" secret: "Segreto" + events: "Quando eseguire il Webhook" active: "Attivo" _events: follow: "Quando segui un profilo" @@ -2510,25 +2339,6 @@ _webhookSettings: renote: "Quando la Nota è Rinotata" reaction: "Quando ricevo una reazione" mention: "Quando mi menzionano" - _systemEvents: - abuseReport: "Quando arriva una segnalazione" - abuseReportResolved: "Quando una segnalazione è risolta" - deleteConfirm: "Vuoi davvero eliminare il Webhook?" -_abuseReport: - _notificationRecipient: - createRecipient: "Aggiungi destinatario della segnalazione" - modifyRecipient: "Modifica destinatario della segnalazione" - recipientType: "Tipo di notifica" - _recipientType: - mail: "Email" - webhook: "Webhook" - _captions: - mail: "Quando ricevi un abuso, notifica l'amministrazione via email" - webhook: "Spedire una notifica al SystemWebhook specificato (sia quando si riceve una segnalazione, che quando viene risolta)" - keywords: "Parole chiave" - notifiedUser: "Profili da notificare" - notifiedWebhook: "Webhook da usare" - deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?" _moderationLogTypes: createRole: "Ruolo creato" deleteRole: "Ruolo eliminato" @@ -2553,7 +2363,6 @@ _moderationLogTypes: resetPassword: "Password azzerata" suspendRemoteInstance: "Istanza remota sospesa" unsuspendRemoteInstance: "Istanza remota riattivata" - updateRemoteInstanceNote: "Aggiornamento del promemoria di moderazione per il server remoto" markSensitiveDriveFile: "File nel Drive segnato come esplicito" unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" resolveAbuseReport: "Segnalazione risolta" @@ -2566,12 +2375,6 @@ _moderationLogTypes: deleteAvatarDecoration: "Eliminazione decorazione della foto profilo" unsetUserAvatar: "Rimossa foto profilo" unsetUserBanner: "Rimossa intestazione profilo" - createSystemWebhook: "Crea un SystemWebhook" - updateSystemWebhook: "Modifica SystemWebhook" - deleteSystemWebhook: "Elimina SystemWebhook" - createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni" - updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni" - deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni" _fileViewer: title: "Dettagli del file" type: "Tipo di file" @@ -2634,72 +2437,3 @@ _dataSaver: _code: title: "Codice evidenziato" description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato." -_hemisphere: - N: "Emisfero boreale" - S: "Emisfero australe" - caption: "Utile per alcune impostazioni del client, per determinare la stagione." -_reversi: - reversi: "Reversi" - gameSettings: "Impostazioni di gioco" - chooseBoard: "Segli la tavola" - blackOrWhite: "Neri / Bianchi" - blackIs: "{name} muove i Neri" - rules: "Regole del gioco" - thisGameIsStartedSoon: "Il gioco sta per iniziare" - waitingForOther: "Attendere l'avversario" - waitingForMe: "Ti stanno aspettando" - waitingBoth: "Preparatevi" - ready: "Pronti" - cancelReady: "Riprendere la preparazione" - opponentTurn: "Turno avversario" - myTurn: "Tocca a te" - turnOf: "Tocca a {name}" - pastTurnOf: "Turno di {name}" - surrender: "Mi arrendo" - surrendered: "Ha ceduto" - timeout: "Tempo scaduto" - drawn: "Pareggio" - won: "Ha vinto {name}" - black: "Neri" - white: "Bianchi" - total: "Totale" - turnCount: "Turno N. {count}" - myGames: "Le mie sfide" - allGames: "Tutte le sfide" - ended: "Conclusione" - playing: "In gioco" - isLlotheo: "Vince chi ha meno pietre (Roseo)" - loopedMap: "Mappa ricorsiva" - canPutEverywhere: "Modalità che può essere posizionata ovunque" - timeLimitForEachTurn: "Tempo limite per turno" - freeMatch: "Sfida libera" - lookingForPlayer: "Alla ricerca di un avversario" - gameCanceled: "Sfida cancellata" - shareToTlTheGameWhenStart: "Pubblica l'inizio della partita sulla tua Timeline" - iStartedAGame: "Inizia la sfida! #MisskeyReversi" - opponentHasSettingsChanged: "L'avversario ha cambiato configurazione" - allowIrregularRules: "Regole inconsuete (completamente libere)" - disallowIrregularRules: "Impedire le regole inconsuete" - showBoardLabels: "Mostra le coordinate del gioco" - useAvatarAsStone: "Immagini profilo come pedine" -_offlineScreen: - title: "Scollegato. Impossibile connettersi al server" - header: "Impossibile connettersi al server" -_urlPreviewSetting: - title: "Impostazioni per l'anteprima delle URL" - enable: "Attiva l'anteprima delle URL" - timeout: "Timeout dell'anteprima in millisecondi" - timeoutDescription: "Impegna al massimo il tempo indicato, altrimenti ignora l'anteprima" - maximumContentLength: "Grandezza del contenuto (Content-Length in byte)" - maximumContentLengthDescription: "Se la grandezza supera il valore, l'anteprima verrà ignorata." - requireContentLength: "Genenerare l'anteprima solo quando è definito Content-Length" - requireContentLengthDescription: "In assenza di questo parametro dal server remoto, l'anteprima verrà ignorata." - userAgent: "User-Agent" - userAgentDescription: "Definire con quale User-Agent si intende identificarsi durante l'acquisizione di un'anteprima. Se è vuoto, useremo il valore predefinito." - summaryProxy: "Endpoint proxy che genera l'anteprima" - summaryProxyDescription: "Genera anteprime utilizzando un proxy Summaly anziché CherryPick." - summaryProxyDescription2: "I parametri sono collegano al proxy come stringa query. Se il proxy non li supporta, verranno ignorati." -_mediaControls: - pip: "Sovraimpressione" - playbackRate: "Velocità di riproduzione" - loop: "Ripetizione infinita" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6389c4affd..f043291a5d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1,6 +1,5 @@ _lang_: "日本語" -forceRenoteVisibilitySelector: "リノートの公開範囲を指定" cherrypickLabs: "CherryPick研究室" cherrypickLabsDescription: "まだ開発中の機能を試してみませんか。一部の機能はちゃんと動かないかもしれません。" copiedLink: "リンクをコピーしました!" @@ -17,7 +16,6 @@ opacity: "不透明度" noteUpdatedAt: "編集済み: {date} {time}" editReaction: "リアクションを編集" removeReaction: "リアクションを削除" -scheduledNoteDelete: "ノートの削除を予約" noNyaization: "ニャ化を表示しない" revertNoNyaization: "ニャ化を含めて表示する" viewTextSource: "テキストのソースを表示する" @@ -138,7 +136,6 @@ copyFileId: "ファイルIDをコピー" copyFolderId: "フォルダーIDをコピー" copyProfileUrl: "プロフィールURLをコピー" searchUser: "ユーザーを検索" -searchThisUsersNotes: "ユーザーのノートを検索" reply: "返信" loadMore: "もっと見る" showMore: "もっと見る" @@ -187,7 +184,6 @@ enterEmoji: "絵文字を入力" renote: "リノート" unrenote: "リノート解除" renoted: "リノートしました。" -renotedToX: "{name} にリノートしました。" quoted: "ノートを引用しました。" replied: "返信を投稿しました。" cantRenote: "この投稿はリノートできません。" @@ -195,8 +191,6 @@ cantReRenote: "リノートをリノートすることはできません。" quote: "引用" inChannelRenote: "チャンネル内リノート" inChannelQuote: "チャンネル内引用" -renoteToChannel: "チャンネルにリノート" -renoteToOtherChannel: "他のチャンネルにリノート" pinnedNote: "ピン留めされたノート" pinned: "ピン留め" you: "あなた" @@ -215,7 +209,6 @@ overwriteFromPinnedEmojis: "全般設定から上書きする" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" rememberNoteVisibility: "公開範囲を記憶する" attachCancel: "添付取り消し" -deleteFile: "ファイルを削除" markAsSensitive: "センシティブとして設定" unmarkAsSensitive: "センシティブを解除する" enterFileName: "ファイル名を入力" @@ -227,20 +220,15 @@ block: "ブロック" unblock: "ブロック解除" suspend: "凍結" unsuspend: "解凍" -setAsSensitive: "センシティブとして設定" -unsetAsSensitive: "センシティブを解除" blockConfirm: "ブロックしますか?" unblockConfirm: "ブロック解除しますか?" suspendConfirm: "凍結しますか?" unsuspendConfirm: "解凍しますか?" -setSensitiveConfirm: "センシティブとして設定しますか?" -unsetSensitiveConfirm: "センシティブを解除しますか?" selectList: "リストを選択" editList: "リストを編集" selectChannel: "チャンネルを選択" selectAntenna: "アンテナを選択" editAntenna: "アンテナを編集" -createAntenna: "アンテナを作成" selectWidget: "ウィジェットを選択" editWidgets: "ウィジェットを編集" editWidgetsExit: "編集を終了" @@ -260,21 +248,13 @@ flagAsBot: "Botとして設定" flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、CherryPickのシステム上での扱いがBotに合ったものになります。" flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!" flagAsCatDescription: "にゃにゃにゃ??" -flagAsSensitive: "センシティブとして設定" -flagAsSensitiveDescription: "このオプションを有効にすると、このアカウントはセンシティブなアカウントとして他者に表示されます。この機能を有効にすると、検索結果に表示されなくなります。現在ローカル限定の機能です。" flagShowTimelineReplies: "タイムラインにノートへの返信を表示する" flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。" autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認" -carefulBot: "Botからのフォローを承認制にする" -carefulBotDescription: "この設定を有効にすると、Botからのフォローリクエストを承認制にします。" addAccount: "アカウントを追加" reloadAccountsList: "アカウントリストの情報を更新" loginFailed: "ログインに失敗しました" showOnRemote: "リモートで表示" -continueOnRemote: "リモートで続行" -chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択" -specifyServerHost: "サーバーのドメインを直接指定" -inputHostName: "ドメインを入力してください" general: "全般" wallpaper: "壁紙" setWallpaper: "壁紙を設定" @@ -285,7 +265,6 @@ followConfirm: "{name}をフォローしますか?" proxyAccount: "プロキシアカウント" proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" host: "ホスト" -selectSelf: "自分を選択" selectUser: "ユーザーを選択" recipient: "宛先" annotation: "注釈" @@ -301,7 +280,6 @@ perDay: "1日ごと" stopActivityDelivery: "アクティビティの配送を停止" blockThisInstance: "このサーバーをブロック" silenceThisInstance: "サーバーをサイレンス" -mediaSilenceThisInstance: "サーバーをメディアサイレンス" operations: "操作" software: "ソフトウェア" version: "バージョン" @@ -322,9 +300,7 @@ clearCachedFilesConfirm: "キャッシュされたリモートファイルをす blockedInstances: "ブロックしたサーバー" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。" silencedInstances: "サイレンスしたサーバー" -silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。" -mediaSilencedInstances: "メディアサイレンスしたサーバー" -mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。" +silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" @@ -395,8 +371,6 @@ termsOfService: "利用規約" start: "始める" home: "ホーム" remoteUserCaution: "リモートユーザーのため、情報が不完全です。" -sensitiveUserCaution: "センシティブなユーザーです。" -deprecatedCaution: "この機能は非推奨です。\n 必要であれば、バックアップを取ることをお勧めします。" activity: "アクティビティ" images: "画像" image: "画像" @@ -418,7 +392,6 @@ selectFile: "ファイルを選択" selectFiles: "ファイルを選択" selectFolder: "フォルダーを選択" selectFolders: "フォルダーを選択" -fileNotSelected: "ファイルが選択されていません" renameFile: "ファイル名を変更" folderName: "フォルダー名" createFolder: "フォルダーを作成" @@ -486,11 +459,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptchaを有効にする" hcaptchaSiteKey: "サイトキー" hcaptchaSecretKey: "シークレットキー" -mcaptcha: "mCaptcha" -enableMcaptcha: "mCaptchaを有効にする" -mcaptchaSiteKey: "サイトキー" -mcaptchaSecretKey: "シークレットキー" -mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHAを有効にする" recaptchaSiteKey: "サイトキー" @@ -506,7 +474,6 @@ name: "名前" antennaSource: "受信ソース" antennaKeywords: "受信キーワード" antennaExcludeKeywords: "除外キーワード" -antennaExcludeBots: "Botアカウントを除外" antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" notifyAntenna: "新しいノートを通知する" withFileAntenna: "ファイルが添付されたノートのみ" @@ -583,12 +550,10 @@ noteOf: "{user}のノート" inviteToGroup: "グループに招待" quoteAttached: "引用付き" quoteQuestion: "引用として添付しますか?" -attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?" noMessagesYet: "まだチャットはありません" newMessageExists: "新しいメッセージがあります" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" -signinRequired: "続行する前に、登録またはログインが必要です" -signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります" +signinRequired: "続行する前に、サインアップまたはサインインが必要です" invitations: "招待" invitationCode: "招待コード" checking: "確認しています" @@ -615,7 +580,6 @@ disableDrawer: "メニューをドロワーで表示しない" youHaveNoGroups: "グループがありません" joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。" showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する" -showReactionsCount: "ノートのリアクション数を表示する" noHistory: "履歴はありません" signinHistory: "ログイン履歴" enableAdvancedMfm: "高度なMFMを有効にする" @@ -761,7 +725,6 @@ medium: "中" small: "小" generateAccessToken: "アクセストークンの発行" permission: "権限" -adminPermission: "管理者権限" enableAll: "全て有効にする" disableAll: "全て無効にする" tokenRequested: "アカウントへのアクセス許可" @@ -800,13 +763,11 @@ channel: "チャンネル" create: "作成" notificationSetting: "通知設定" notificationSettingDesc: "表示する通知の種別を選択してください。" -notificationFlush: "通知を全部削除" useGlobalSetting: "グローバル設定を使う" useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。" other: "その他" regenerateLoginToken: "ログイントークンを再生成" regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" -theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" fileIdOrUrl: "ファイルIDまたはURL" behavior: "動作" @@ -962,7 +923,6 @@ administration: "管理" accounts: "アカウント" switch: "切り替え" noMaintainerInformationWarning: "管理者情報が設定されていません。" -noInquiryUrlWarning: "問い合わせ先URLが設定されていません。" noBotProtectionWarning: "Botプロテクションが設定されていません。" configure: "設定する" postToGallery: "ギャラリーへ投稿" @@ -1127,7 +1087,6 @@ neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "CherryPickを気に入っていただけましたか?" pleaseDonate: "CherryPickは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!" -correspondingSourceIsAvailable: "対応するソースコードは{anchor}から利用可能です。" roles: "ロール" role: "ロール" noRole: "ロールはありません" @@ -1154,8 +1113,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめる" thisPostMayBeAnnoyingIgnore: "このまま投稿" -collapseRenotes: "リノートのスマート省略" -collapseRenotesDescription: "リアクションやリノートをしたことがあるノートをたたんで表示します。" +collapseRenotes: "見たことのあるリノートを省略して表示" collapseDefault: "特定のMFM構文を含むノートを省略して表示" internalServerError: "サーバー内部エラー" internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。" @@ -1180,9 +1138,6 @@ resetPasswordConfirm: "パスワードリセットしますか?" sensitiveWords: "センシティブワード" sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" -prohibitedWords: "禁止ワード" -prohibitedWordsDescription: "設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。" -prohibitedWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" hiddenTags: "非表示ハッシュタグ" hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" @@ -1201,8 +1156,6 @@ limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小し noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" -audio: "音声" -audioFiles: "音声" dataSaver: "データセーバー" accountMigration: "アカウントの移行" accountMoved: "このユーザーは新しいアカウントに移行しました:" @@ -1211,7 +1164,7 @@ operationForbidden: "この操作はできません" forceShowAds: "常に広告を表示する" event: "イベント" events: "イベント" -reverseChronological: "終了済み" +reverseChronological: "倒叙" addMemo: "メモを追加" editMemo: "メモを編集" reactionsList: "リアクション一覧" @@ -1233,8 +1186,6 @@ preservedUsernames: "予約ユーザー名" preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。" createNoteFromTheFile: "このファイルからノートを作成" archive: "アーカイブ" -archived: "アーカイブ済み" -unarchive: "アーカイブ解除" channelArchiveConfirmTitle: "{name}をアーカイブしますか?" channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。" thisChannelArchived: "このチャンネルはアーカイブされています。" @@ -1245,9 +1196,6 @@ preventAiLearning: "生成AIによる学習を拒否" preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。" options: "オプション" specifyUser: "ユーザー指定" -lookupConfirm: "照会しますか?" -openTagPageConfirm: "ハッシュタグのページを開きますか?" -specifyHost: "ホスト指定" failedToPreviewUrl: "プレビューできません" update: "更新" rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール" @@ -1309,7 +1257,6 @@ showRenotes: "リノートを表示" edited: "編集済み" notificationRecieveConfig: "通知の受信設定" mutualFollow: "相互フォロー" -followingOrFollower: "フォロー中またはフォロワー" fileAttachedOnly: "ファイル付きのみ" showRepliesToOthersInTimeline: "TLに他の人への返信を含める" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" @@ -1318,20 +1265,12 @@ hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返 confirmShowRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか?" confirmHideRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか?" externalServices: "外部サービス" -sourceCode: "ソースコード" -sourceCodeIsNotYetProvided: "ソースコードはまだ提供されていません。この問題の修正について管理者に問い合わせてください。" -repositoryUrl: "リポジトリURL" -repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入します。CherryPickを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/kokonect-link/cherrypick と記入します。" -repositoryUrlOrTarballRequired: "リポジトリを公開していない場合、代わりにtarballを提供する必要があります。詳細は.config/example.ymlを参照してください。" -feedback: "フィードバック" -feedbackUrl: "フィードバックURL" impressum: "運営者情報" impressumUrl: "運営者情報URL" impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。" privacyPolicy: "プライバシーポリシー" privacyPolicyUrl: "プライバシーポリシーURL" tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" -statusUrl: "ステータスページ" avatarDecorations: "アイコンデコレーション" attach: "付ける" detach: "外す" @@ -1355,46 +1294,12 @@ seasonalScreenEffect: "季節に応じた画面の演出" decorate: "デコる" addMfmFunction: "装飾を追加" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" -bubbleGame: "バブルゲーム" -sfx: "効果音" -soundWillBePlayed: "サウンドが再生されます" -showReplay: "リプレイを見る" -replay: "リプレイ" -replaying: "リプレイ中" -endReplay: "リプレイを終了" -copyReplayData: "リプレイデータをコピー" -ranking: "ランキング" -lastNDays: "直近{n}日" -backToTitle: "タイトルへ" -hemisphere: "お住まいの地域" -withSensitive: "センシティブなファイルを含むノートを表示" -userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" -enableHorizontalSwipe: "スワイプしてタブを切り替える" -loading: "読み込み中" -surrender: "やめる" -gameRetry: "リトライ" -notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください" -useTotp: "ワンタイムパスワードを使う" -useBackupCode: "バックアップコードを使う" -launchApp: "アプリを起動" -useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する" -keepOriginalFilename: "オリジナルのファイル名を保持" -keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられます。" -noDescription: "説明文はありません" -alwaysConfirmFollow: "フォローの際常に確認する" -inquiry: "お問い合わせ" -tryAgain: "もう一度お試しください。" -confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する" -sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?" -createdLists: "作成したリスト" -createdAntennas: "作成したアンテナ" showUnreadNotificationsCount: "未読の通知の数を表示する" -showCatOnly: "ネコミミ付きのみ" +showCatOnly: "キャット付きのみ" additionalPermissionsForFlash: "Playへの追加許可" thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています" doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか?" translateProfile: "プロフィールを翻訳する" -getQrCode: "QRコードを取得" _nsfwOpenBehavior: click: "タップして開く" @@ -1416,32 +1321,6 @@ _showingAnimatedImages: _messaging: direct: "ダイレクトメッセージ" -_delivery: - status: "配信状態" - stop: "配信停止" - resume: "配信再開" - _type: - none: "配信中" - manuallySuspended: "手動停止中" - goneSuspended: "サーバー削除のため停止中" - autoSuspendedForNotResponding: "サーバー応答なしのため停止中" - -_bubbleGame: - howToPlay: "遊び方" - hold: "ホールド" - _score: - score: "スコア" - scoreYen: "稼いだ金額" - highScore: "ハイスコア" - maxChain: "最大チェーン数" - yen: "{yen}円" - estimatedQty: "{qty}個分" - scoreSweets: "おにぎり {onigiriQtyWithUnit}" - _howToPlay: - section1: "位置を調整してハコにモノを落とします。" - section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。" - section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!" - _announcement: forExistingUsers: "既存ユーザーのみ" forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" @@ -1451,7 +1330,7 @@ _announcement: tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。" readConfirmTitle: "既読にしますか?" readConfirmText: "「{title}」の内容を読み、既読にします。" - shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、常時掲示するための情報ではなく、即時性が求められる情報の掲示のためにお知らせを使用することを推奨します。" + shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。" dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。" silence: "非通知" silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。" @@ -1474,10 +1353,6 @@ _cherrypick: showRenoteConfirmPopupDescription: "この設定は「全般 - リノートと引用ボタンを分けて表示する」設定がオンになっている必要があります。" expandOnNoteClick: "クリックでノートの詳細を開く" expandOnNoteClickDescription: "オフの場合、ノートメニューの[詳細]をクリックするか、日付をクリックして開けます。" - expandOnNoteClickBehavior: "ノートをクリックして開くとき" - _expandOnNoteClickBehavior: - click: "クリックして開く" - doubleClick: "ダブルクリックで開く" displayHeaderNavBarWhenScroll: "スクロール時の要素表示(ヘッダー、フローティングボタン、ナビゲーションバー)" _displayHeaderNavBarWhenScroll: all: "全て表示" @@ -1623,11 +1498,11 @@ _event: ticketsUrl: "チケット" isFree: "無料" price: "価格" - availability: "予約可能" + availability: "可用性" from: "から" until: "まで" - availabilityStart: "予約開始" - availabilityEnd: "予約終了" + availabilityStart: "アベイラビリティ開始" + availabilityEnd: "アベイラビリティ終了" keywords: "キーワード" performers: "出演者" @@ -1643,8 +1518,6 @@ _serverSettings: fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。" fanoutTimelineDbFallback: "データベースへのフォールバック" fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" - inquiryUrl: "問い合わせ先URL" - inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" _accountMigration: moveFrom: "別のアカウントからこのアカウントに移行" @@ -1718,7 +1591,7 @@ _achievements: _login3: title: "ビギナーⅠ" description: "通算ログイン日数が3日" - flavor: "今日からね僕は チェリーピキストってことで" + flavor: "今日からね僕は ミスキストってことで" _login7: title: "ビギナーⅡ" description: "通算ログイン日数が7日" @@ -1880,10 +1753,6 @@ _achievements: _setNameToNoriDev: title: "神様コンプレックス(CherryPick)" description: "名前を noridev に設定した" - _setNameToYojo: - title: "ロリータコンプレックス" - description: "名前を 幼女 に設定した" - flavor: "これであなたもロリコン" _passedSinceAccountCreated1: title: "一周年" description: "アカウント作成から1年経過した" @@ -1914,29 +1783,6 @@ _achievements: _tutorialCompleted: title: "CherryPick初心者講座 修了証" description: "チュートリアルを完了した" - _bubbleGameExplodingHead: - title: "🤯" - description: "バブルゲームで最も大きいモノを出した" - _bubbleGameDoubleExplodingHead: - title: "ダブル🤯" - description: "バブルゲームで最も大きいモノを2つ同時に出した" - flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて" - _ohayoujo1: - title: "今日も一日!" - description: "おはようようじょー!と投稿した" - flavor: "おはようじょー!" - _ohayoujo7: - title: "元気いっぱい!" - description: "おはようようじょー!と7日投稿した" - flavor: "おはようじょー!" - _ohayoujo30: - title: "笑顔満点" - description: "おはようようじょー!と30日投稿した" - flavor: "おはようじょー!" - _ohayoujo365: - title: "皆勤賞" - description: "おはようようじょー!と365日投稿した" - flavor: "おはようじょー!" _role: new: "ロールの作成" @@ -1979,7 +1825,6 @@ _role: ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" canEditNote: "ノートの編集" - mentionMax: "ノート内の最大メンション数" canInvite: "サーバー招待コードの発行" inviteLimit: "招待コードの作成可能数" inviteLimitCycle: "招待コードの発行間隔" @@ -1987,9 +1832,7 @@ _role: canManageCustomEmojis: "カスタム絵文字の管理" canManageAvatarDecorations: "アバターデコレーションの管理" driveCapacity: "ドライブ容量" - fileSizeLimit: "ファイルサイズ上限" alwaysMarkNsfw: "ファイルにNSFWを常に付与" - canUpdateBioMedia: "アイコンとバナーの更新を許可" pinMax: "ノートのピン留めの最大数" antennaMax: "アンテナの作成可能数" wordMuteMax: "ワードミュートの最大文字数" @@ -2002,18 +1845,11 @@ _role: descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" canHideAds: "広告の非表示" canSearchNotes: "ノート検索の利用" - canAdvancedSearchNotes: "高度な検索の利用" canUseTranslator: "翻訳機能の利用" avatarDecorationLimit: "アイコンデコレーションの最大取付個数" _condition: - roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" isRemote: "リモートユーザー" - isCat: "猫ユーザー" - isBot: "botユーザー" - isSuspended: "サスペンド済みユーザー" - isLocked: "鍵アカウントユーザー" - isExplorable: "「アカウントを見つけやすくする」が有効なユーザー" createdLessThan: "アカウント作成から~以内" createdMoreThan: "アカウント作成から~経過" followersLessThanOrEq: "フォロワー数が~以下" @@ -2098,7 +1934,6 @@ _plugin: installWarn: "信頼できないプラグインはインストールしないでください。" manage: "プラグインの管理" viewSource: "ソースを表示" - viewLog: "ログを表示" _preferencesBackups: list: "作成したバックアップ" @@ -2131,8 +1966,6 @@ _aboutMisskey: contributors: "コントリビューター" allContributors: "全てのコントリビューター" source: "ソースコード" - original: "オリジナル" - thisIsModifiedVersion: "{name}はオリジナルのCherryPickを改変したバージョンを使用しています。" translation: "Misskeyを翻訳" donate: "Misskeyに寄付" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" @@ -2173,7 +2006,7 @@ _mfm: inlineCode: "コード(インライン)" inlineCodeDescription: "プログラムなどのコードをインラインでシンタックスハイライトします。" blockCode: "コード(ブロック)" - blockCodeDescription: "複数行のプログラムなどのコードをブロックでシンタックスハイライトします。いくつかの言語を指定するとその言語に合わせたシンタックスハイライトになります。" + blockCodeDescription: "複数行のプログラムなどのコードをブロックでシンタックスハイライトします。" inlineMath: "数式(インライン)" inlineMathDescription: "数式(KaTeX)をインラインで表示します。" blockMath: "数式(ブロック)" @@ -2230,8 +2063,6 @@ _mfm: plainDescription: "内側の構文を全て無効にします。" ruby: "ルビ" rubyDescription: "文字の上にルビを表示します。" - border: "ボーダー" - borderDescription: "内容を枠線で囲むことができます。" _instanceTicker: none: "表示しない" @@ -2355,6 +2186,8 @@ _sfx: notification: "通知" chat: "チャット" chatBg: "チャット(バックグラウンド)" + antenna: "アンテナ受信" + channel: "チャンネル通知" reaction: "リアクション選択時" _soundSettings: @@ -2364,7 +2197,6 @@ _soundSettings: driveFileTypeWarnDescription: "音声ファイルを選択してください" driveFileDurationWarn: "音声が長すぎます" driveFileDurationWarnDescription: "長い音声を使用するとCherryPickの使用に支障をきたす可能性があります。それでも続行しますか?" - driveFileError: "音声が読み込めませんでした。設定を変更してください" _ago: future: "未来" @@ -2397,7 +2229,8 @@ _2fa: alreadyRegistered: "既に設定は完了しています。" registerTOTP: "認証アプリの設定を開始" step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。" - step2: "次に、表示されているQRコードをアプリでスキャンするか、ボタンをクリックして端末上でアプリを開きます。" + step2: "次に、表示されているQRコードをアプリでスキャンします。" + step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。" step2Uri: "デスクトップアプリを使用する場合は次のURIを入力します" step3Title: "確認コードを入力" step3: "アプリに表示されている確認コード(トークン)を入力します。" @@ -2421,7 +2254,6 @@ _2fa: backupCodesDescription: "認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。" backupCodeUsedWarning: "バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。" backupCodesExhaustedWarning: "バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。" - moreDetailedGuideHere: "詳細なガイドはこちら" _permissions: "read:account": "アカウントの情報を見る" @@ -2473,6 +2305,7 @@ _permissions: "read:admin:server-info": "サーバーの情報を見る" "read:admin:show-moderation-log": "モデレーションログを見る" "read:admin:show-user": "ユーザーのプライベートな情報を見る" + "read:admin:show-users": "ユーザーのプライベートな情報を見る" "write:admin:suspend-user": "ユーザーを凍結する" "write:admin:unset-user-avatar": "ユーザーのアバターを削除する" "write:admin:unset-user-banner": "ユーザーのバーナーを削除する" @@ -2568,8 +2401,6 @@ _widgets: chooseList: "リストを選択" clicker: "クリッカー" birthdayFollowings: "今日誕生日のユーザー" - dice: "サイコロ" - search: "検索" _cw: hide: "隠す" @@ -2609,8 +2440,6 @@ _visibility: followersDescription: "自分のフォロワーのみに公開" specified: "ダイレクト" specifiedDescription: "指定したユーザーのみに公開" - private: "プライベート" - privateDescription: "あなたのみが見れます" disableFederation: "連合なし" disableFederationDescription: "他サーバーへの配信を行いません" @@ -2645,7 +2474,6 @@ _profile: _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" - clips: "クリップ" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" @@ -2685,7 +2513,6 @@ _instanceCharts: _timelines: home: "ホーム" local: "ローカル" - media: "メディア" social: "ソーシャル" global: "グローバル" @@ -2704,7 +2531,6 @@ _play: title: "タイトル" script: "スクリプト" summary: "説明" - visibilityDescription: "非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。" _pages: newPage: "ページの作成" @@ -2750,8 +2576,6 @@ _pages: section: "セクション" image: "画像" button: "ボタン" - dynamic: "動的ブロック" - dynamicDescription: "このブロックは廃止されています。今後は{play}を利用してください。" note: "ノート埋め込み" _note: @@ -2786,10 +2610,8 @@ _notification: sendTestNotification: "テスト通知を送信する" notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" reactedBySomeUsers: "{n}人がリアクションしました" - likedBySomeUsers: "{n}人がいいねしました" renotedBySomeUsers: "{n}人がリノートしました" followedBySomeUsers: "{n}人にフォローされました" - flushNotification: "通知の履歴をリセットする" _types: all: "すべて" @@ -2817,7 +2639,6 @@ _deck: alwaysShowMainColumn: "常にメインカラムを表示" columnAlign: "カラムの寄せ" addColumn: "カラムを追加" - newNoteNotificationSettings: "新着ノート通知の設定" configureColumn: "カラムの設定" swapLeft: "左に移動" swapRight: "右に移動" @@ -2861,10 +2682,9 @@ _drivecleaner: _webhookSettings: createWebhook: "Webhookを作成" - modifyWebhook: "Webhookを編集" name: "名前" secret: "シークレット" - trigger: "トリガー" + events: "Webhookを実行するタイミング" active: "有効" _events: follow: "フォローしたとき" @@ -2874,27 +2694,6 @@ _webhookSettings: renote: "Renoteされたとき" reaction: "リアクションがあったとき" mention: "メンションされたとき" - _systemEvents: - abuseReport: "ユーザーから通報があったとき" - abuseReportResolved: "ユーザーからの通報を処理したとき" - userCreated: "ユーザーが作成されたとき" - deleteConfirm: "Webhookを削除しますか?" - -_abuseReport: - _notificationRecipient: - createRecipient: "通報の通知先を追加" - modifyRecipient: "通報の通知先を編集" - recipientType: "通知先の種類" - _recipientType: - mail: "メール" - webhook: "Webhook" - _captions: - mail: "モデレーター権限を持つユーザーのメールアドレスに通知を送ります(通報を受けた時のみ)" - webhook: "指定したSystemWebhookに通知を送ります(通報を受けた時と通報を解決した時にそれぞれ発信)" - keywords: "キーワード" - notifiedUser: "通知先ユーザー" - notifiedWebhook: "使用するWebhook" - deleteConfirm: "通知先を削除しますか?" _moderationLogTypes: createRole: "ロールを作成" @@ -2908,7 +2707,7 @@ _moderationLogTypes: updateCustomEmoji: "カスタム絵文字更新" deleteCustomEmoji: "カスタム絵文字削除" updateServerSettings: "サーバー設定更新" - updateUserNote: "ユーザーのモデレーションノート更新" + updateUserNote: "モデレーションノート更新" deleteDriveFile: "ファイルを削除" deleteNote: "ノートを削除" createGlobalAnnouncement: "全体のお知らせを作成" @@ -2920,7 +2719,6 @@ _moderationLogTypes: resetPassword: "パスワードをリセット" suspendRemoteInstance: "リモートサーバーを停止" unsuspendRemoteInstance: "リモートサーバーを再開" - updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新" markSensitiveDriveFile: "ファイルをセンシティブ付与" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" resolveAbuseReport: "通報を解決" @@ -2933,12 +2731,6 @@ _moderationLogTypes: deleteAvatarDecoration: "アイコンデコレーションを削除" unsetUserAvatar: "ユーザーのアイコンを解除" unsetUserBanner: "ユーザーのバナーを解除" - createSystemWebhook: "SystemWebhookを作成" - updateSystemWebhook: "SystemWebhookを更新" - deleteSystemWebhook: "SystemWebhookを削除" - createAbuseReportNotificationRecipient: "通報の通知先を作成" - updateAbuseReportNotificationRecipient: "通報の通知先を更新" - deleteAbuseReportNotificationRecipient: "通報の通知先を削除" _fileViewer: title: "ファイルの詳細" @@ -2994,98 +2786,18 @@ _externalResourceInstaller: _dataSaver: _media: - title: "メディアの読み込みを無効化" + title: "メディアの読み込み" description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。" _avatar: - title: "アイコン画像のアニメーションを無効化" + title: "アイコン画像" description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。" _urlPreview: - title: "URLプレビューのサムネイルを非表示" + title: "URLプレビューのサムネイル" description: "URLプレビューのサムネイル画像が読み込まれなくなります。" _code: - title: "コードハイライトを非表示" + title: "コードハイライト" description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" -_hemisphere: - N: "北半球" - S: "南半球" - caption: "一部のクライアント設定で、季節を判定するために使用します。" - -_reversi: - reversi: "リバーシ" - gameSettings: "対局の設定" - chooseBoard: "ボードを選択" - blackOrWhite: "先行/後攻" - blackIs: "{name}が黒(先行)" - rules: "ルール" - thisGameIsStartedSoon: "対局はまもなく開始されます" - waitingForOther: "相手の準備が完了するのを待っています" - waitingForMe: "あなたの準備が完了するのを待っています" - waitingBoth: "準備してください" - ready: "準備完了" - cancelReady: "準備を再開" - opponentTurn: "相手のターンです" - myTurn: "あなたのターンです" - turnOf: "{name}のターンです" - pastTurnOf: "{name}のターン" - surrender: "投了" - surrendered: "投了により" - timeout: "時間切れ" - drawn: "引き分け" - won: "{name}の勝ち" - black: "黒" - white: "白" - total: "合計" - turnCount: "{count}ターン目" - myGames: "自分の対局" - allGames: "みんなの対局" - ended: "終了" - playing: "対局中" - isLlotheo: "石の少ない方が勝ち(ロセオ)" - loopedMap: "ループマップ" - canPutEverywhere: "どこでも置けるモード" - timeLimitForEachTurn: "1ターンの時間制限" - freeMatch: "フリーマッチ" - lookingForPlayer: "対戦相手を探しています" - gameCanceled: "対局がキャンセルされました" - shareToTlTheGameWhenStart: "開始時に対局をタイムラインに投稿" - iStartedAGame: "対局を開始しました! #MisskeyReversi" - opponentHasSettingsChanged: "相手が設定を変更しました" - allowIrregularRules: "変則許可 (完全フリー)" - disallowIrregularRules: "変則なし" - showBoardLabels: "盤面に行・列番号を表示" - useAvatarAsStone: "石をアイコンにする" - -_offlineScreen: - title: "オフライン - サーバーに接続できません" - header: "サーバーに接続できません" - -_urlPreviewSetting: - title: "URLプレビューの設定" - enable: "URLプレビューを有効にする" - timeout: "プレビュー取得時のタイムアウト(ms)" - timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。" - maximumContentLength: "Content-Lengthの最大値(byte)" - maximumContentLengthDescription: "Content-Lengthがこの値を超えた場合、プレビューは生成されません。" - requireContentLength: "Content-Lengthが取得できた場合のみプレビューを生成" - requireContentLengthDescription: "相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。" - userAgent: "User-Agent" - userAgentDescription: "プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。" - summaryProxy: "プレビューを生成するプロキシのエンドポイント" - summaryProxyDescription: "CherryPick本体ではなく、サマリープロキシを使用してプレビューを生成します。" - summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。" - -_mediaControls: - pip: "ピクチャインピクチャ" - playbackRate: "再生速度" - loop: "ループ再生" - -_contextMenu: - title: "コンテキストメニュー" - app: "アプリケーション" - appWithShift: "Shiftキーでアプリケーション" - native: "ブラウザのUI" - _abuse: _resolver: 1hour: "一時間" @@ -3111,46 +2823,3 @@ _imageCompressionMode: noResizeCompress: "縮小せず再圧縮する" resizeCompressLossy: "縮小して非可逆圧縮する" noResizeCompressLossy: "縮小せず非可逆圧縮する" - -_advancedSearch: - _fileOption: - title: "ファイルの添付状態" - fileAttachedOnly: "あり" - noFile: "なし" - combined: "全て" - _searchOption: - toggleCW: "CW付きを除外する" - toggleReply: "リプライを除外する" - toggleDate: "日時を指定する" - toggleAdvancedSearch: "高度な検索を有効にする" - toggleQuote: "引用を除外する" - _specifyDate: - startDate: "から" - endDate: "まで" - _description: - other: "その他の設定" - _fileNsfwOption: - title: "添付ファイルのセンシティブ状態" - combined: "フィルタしない" - withOutSensitive: "除外" - includeSensitive: "含むもの" - sensitiveOnly: "全てセンシティブ" - -_searchOrApShow: - question: "照会を行いますか?" - search: "検索" - lookup: "照会" - -_dice: - rollDice: "サイコロを振る" - diceCount: "サイコロの数" - diceFaces: "サイコロの面数" - -_isIndexable: - title: "公開ノートをインデックス化" - description: "kmy互換機能。公開ノートをインデックス化するかどうかを設定します。" - -_altWarning: - noAltWarning: "ファイルに代替テキストが設定されていません。" - showNoAltWarning: "画像に代替テキストが設定されていない場合に警告を表示する" - postAnyWay: "投稿フォームへ" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 767dd62bf6..990c1bfba0 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -108,7 +108,6 @@ enterEmoji: "絵文字を入れてや" renote: "リノート" unrenote: "リノートやめる" renoted: "リノートしたで。" -renotedToX: "{name}にリノートしたで" cantRenote: "この投稿はリノートできへんっぽい。" cantReRenote: "リノート自体はリノートできへんで。" quote: "引用" @@ -131,7 +130,6 @@ overwriteFromPinnedEmojis: "全般設定から上書きする" reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。" rememberNoteVisibility: "公開範囲覚えといて" attachCancel: "のっけるのやめる" -deleteFile: "ファイルをほかす" markAsSensitive: "ちょっとこれはアカン" unmarkAsSensitive: "そこまでアカンことないやろ" enterFileName: "ファイル名を入れてや" @@ -314,7 +312,6 @@ selectFile: "ファイル選んでや" selectFiles: "ファイル選んでや" selectFolder: "フォルダ選んでや" selectFolders: "フォルダ選んでや" -fileNotSelected: "ファイルが選択されてへんで" renameFile: "ファイル名をいらう" folderName: "フォルダー名" createFolder: "フォルダー作る" @@ -382,11 +379,6 @@ hcaptcha: "hCaptcha(キャプチャ)" enableHcaptcha: "hCaptcha(キャプチャ)をつけとく" hcaptchaSiteKey: "サイトキー" hcaptchaSecretKey: "シークレットキー" -mcaptcha: "mCaptcha" -enableMcaptcha: "hCaptcha(キャプチャ)をつけとく" -mcaptchaSiteKey: "サイトキー" -mcaptchaSecretKey: "シークレットキー" -mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA(リキャプチャ)を有効にする" recaptchaSiteKey: "サイトキー" @@ -402,7 +394,6 @@ name: "名前" antennaSource: "受信ソース(このソースは食われへん)" antennaKeywords: "受信キーワード" antennaExcludeKeywords: "除外キーワード" -antennaExcludeBots: "Botアカウントを除外" antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や" notifyAntenna: "新しいノートを通知すんで" withFileAntenna: "なんか添付されたノートだけ" @@ -479,7 +470,6 @@ noteOf: "{user}はんのノート" inviteToGroup: "グループに招く" quoteAttached: "引用付いとるで" quoteQuestion: "引用として添付してもええか?" -attachAsFileQuestion: "クリップボードのテキストが長すぎるからテキストファイルとして添付してもええか?" noMessagesYet: "まだチャットはあらへんで" newMessageExists: "新しいメッセージがきたで" onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。" @@ -510,7 +500,6 @@ disableDrawer: "メニューをドロワーで表示せえへん" youHaveNoGroups: "グループがあらへんねぇ。" joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループ作ってからやってな" showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで" -showReactionsCount: "ノートのリアクション数を表示する" noHistory: "履歴はないわ。" signinHistory: "ログイン履歴" enableAdvancedMfm: "ややこしいMFMもありにする" @@ -649,7 +638,6 @@ medium: "中" small: "小" generateAccessToken: "アクセストークンの発行" permission: "権限" -adminPermission: "管理者権限" enableAll: "全部使えるようにする" disableAll: "全部使えへんようにする" tokenRequested: "アカウントへのアクセス許してやったらどうや" @@ -688,13 +676,11 @@ channel: "チャンネル" create: "作成" notificationSetting: "通知設定" notificationSettingDesc: "出す通知の種類えらんでや。" -notificationFlush: "通知全部消したる" useGlobalSetting: "グローバル設定を使ってや" useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使われるで。オフにすると、別々に設定できるようになるで。" other: "その他" regenerateLoginToken: "ログイントークンを再生成" regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。" -theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を探すときのキーワードになるで。" setMultipleBySeparatingWithSpace: "スペースで区切って何個でも設定できるで。" fileIdOrUrl: "ファイルIDかURL" behavior: "動作" @@ -848,7 +834,6 @@ administration: "管理" accounts: "アカウント" switch: "切り替え" noMaintainerInformationWarning: "管理者情報が設定されてへんで" -noInquiryUrlWarning: "問い合わせ先URLが設定されてへんで。" noBotProtectionWarning: "Botプロテクションが設定されてへんで。" configure: "設定する" postToGallery: "ギャラリーへ投稿" @@ -908,8 +893,6 @@ makeReactionsPublicDescription: "あんたがしたツッコミ一覧を誰で classic: "クラシック" muteThread: "スレッドをミュート" unmuteThread: "スレッドのミュートを解除" -followingVisibility: "フォローの公開範囲" -followersVisibility: "フォロワーの公開範囲" continueThread: "さらにスレッドを見るで" deleteAccountConfirm: "アカウントを消すで?ええんか?" incorrectPassword: "パスワードがちゃうわ。" @@ -1012,7 +995,6 @@ neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "CherryPick気に入ってくれた?" pleaseDonate: "CherryPickは{host}が使うとる無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。" -correspondingSourceIsAvailable: "{anchor}" roles: "ロール" role: "ロール" noRole: "ロールはありまへん" @@ -1063,9 +1045,6 @@ resetPasswordConfirm: "パスワード作り直すんでええな?" sensitiveWords: "けったいな単語" sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。" sensitiveWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。" -prohibitedWords: "禁止ワード" -prohibitedWordsDescription: "設定した言葉が含まれるノートを投稿しようとしたら、エラーが出るようにするで。改行で区切って複数設定できるで。" -prohibitedWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。" hiddenTags: "見えてへんハッシュタグ" hiddenTagsDescription: "設定したタグを最近流行りのとこに見えんようにすんで。複数設定するときは改行で区切ってな。" notesSearchNotAvailable: "なんかノート探せへん。" @@ -1084,8 +1063,6 @@ limitWidthOfReaction: "ツッコミの最大横幅を制限して、ちっさく noteIdOrUrl: "ノートIDかURL" video: "動画" videos: "動画" -audio: "音声" -audioFiles: "音声" dataSaver: "データケチケチ" accountMigration: "アカウントのお引っ越し" accountMoved: "このユーザーはさらのアカウントに引っ越したで:" @@ -1181,7 +1158,6 @@ showRenotes: "リノート出す" edited: "いじったやつ" notificationRecieveConfig: "通知もらうかの設定" mutualFollow: "お互いフォローしてんで" -followingOrFollower: "フォロー中またはフォロワー" fileAttachedOnly: "ファイルのっけてあるやつだけ" showRepliesToOthersInTimeline: "タイムラインに他の人への返信とかも入れるで" hideRepliesToOthersInTimeline: "タイムラインに他の人への返信とかは入れへん" @@ -1190,13 +1166,6 @@ hideRepliesToOthersInTimelineAll: "タイムラインに今フォローしとる confirmShowRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れるか?" confirmHideRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れへんのか?" externalServices: "他のサイトのサービス" -sourceCode: "ソースコード" -sourceCodeIsNotYetProvided: "ソースコードはまだ提供されてへんで。問題の修正について管理者に問い合わせてみ。" -repositoryUrl: "リポジトリURL" -repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入するで。CherryPickをそのまんま(ソースコードにいかなる変更も加えずに)使っとる場合は https://github.com/kokonect-link/cherrypick と記入するで。" -repositoryUrlOrTarballRequired: "リポジトリを公開してへんなら、代わりにtarballを提供する必要があるで。詳細は.config/example.ymlを参照してな。" -feedback: "フィードバック" -feedbackUrl: "フィードバックURL" impressum: "運営者の情報" impressumUrl: "運営者の情報URL" impressumDescription: "ドイツとかの一部んところではな、表示が義務付けられてんねん(Impressum)。" @@ -1224,55 +1193,6 @@ remainingN: "残り:{n}" overwriteContentConfirm: "今の内容に上書きされるけどいい?" seasonalScreenEffect: "季節にあった画面の動き" decorate: "デコる" -addMfmFunction: "装飾つける" -enableQuickAddMfmFunction: "ややこしいMFMのピッカーを出す" -bubbleGame: "バブルゲーム" -sfx: "効果音" -soundWillBePlayed: "サウンドが再生されるで" -showReplay: "リプレイ見る" -replay: "リプレイ" -replaying: "リプレイ中" -endReplay: "リプレイを終了" -copyReplayData: "リプレイデータをコピー" -ranking: "ランキング" -lastNDays: "直近{n}日" -backToTitle: "タイトルへ" -hemisphere: "住んでる地域" -withSensitive: "センシティブなファイルを含むノートを表示" -userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" -enableHorizontalSwipe: "スワイプしてタブを切り替える" -loading: "読み込み中" -surrender: "やめとく" -gameRetry: "もういっちょ" -notUsePleaseLeaveBlank: "使用せえへん場合は空欄にしてや" -useTotp: "ワンタイムパスワードを使う" -useBackupCode: "バックアップコードを使う" -launchApp: "アプリを起動" -useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する" -keepOriginalFilename: "オリジナルのファイル名を保持" -keepOriginalFilenameDescription: "この設定をオフにすると、アップロード時にファイル名が自動でランダム文字列に置き換えられるで。" -noDescription: "説明文はあらへんで" -alwaysConfirmFollow: "フォローの際常に確認する" -inquiry: "問い合わせ" -_delivery: - stop: "配信せぇへん" - _type: - none: "配信しとる" -_bubbleGame: - howToPlay: "遊び方" - hold: "ホールド" - _score: - score: "スコア" - scoreYen: "稼いだ金額" - highScore: "ハイスコア" - maxChain: "最大チェーン数" - yen: "{yen}円" - estimatedQty: "{qty}個分" - scoreSweets: "おにぎり {onigiriQtyWithUnit}" - _howToPlay: - section1: "位置を調整してハコにモノを落とすで。" - section2: "同じもんがくっついたら別のやつになって、スコアがもらえるで。" - section3: "モノがハコからあふれたらゲームオーバーや。ハコからあふれんようにしながらモノを融合させてハイスコアを目指しいや!" _announcement: forExistingUsers: "もうおるユーザーのみ" forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。" @@ -1454,7 +1374,7 @@ _achievements: _login3: title: "ビギナーⅠ" description: "通算3日ログインした" - flavor: "今日からワシはチェリーピキストやで" + flavor: "今日からワシはミスキストやで" _login7: title: "ビギナーⅡ" description: "通算7日ログインした" @@ -1559,7 +1479,7 @@ _achievements: _viewAchievements3min: title: "実績好き" description: "実績一覧を3分以上眺め続けた" - _iLoveCherryPick: + _iLoveMisskey: title: "CherryPick好きやねん" description: "\"I ❤ #CherryPick\"を投稿した" flavor: "CherryPickを使ってくれておおきにな~ by 開発チーム" @@ -1646,13 +1566,6 @@ _achievements: _tutorialCompleted: title: "CherryPickひよっこ講座 修了証" description: "チュートリアル全部やった" - _bubbleGameExplodingHead: - title: "🤯" - description: "バブルゲームで最も大きいモノを出した" - _bubbleGameDoubleExplodingHead: - title: "ダブル🤯" - description: "バブルゲームで最も大きいモノを2つ同時に出した" - flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて" _role: new: "ロールの作成" edit: "ロールの編集" @@ -1693,7 +1606,6 @@ _role: gtlAvailable: "グローバルタイムライン見る" ltlAvailable: "ローカルタイムライン見る" canPublicNote: "パブリック投稿できるか" - mentionMax: "ノート内の最大メンション数" canInvite: "サーバー招待コード作る" inviteLimit: "招待コード作れる数" inviteLimitCycle: "招待コードの作れる間隔" @@ -1717,14 +1629,8 @@ _role: canUseTranslator: "翻訳使えるかどうか" avatarDecorationLimit: "アイコンデコのいっちばんつけれる数" _condition: - roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" isRemote: "リモートユーザー" - isCat: "猫ユーザー" - isBot: "botユーザー" - isSuspended: "サスペンド済みユーザー" - isLocked: "鍵アカウントユーザー" - isExplorable: "「アカウントを見つけやすくする」が有効なユーザー" createdLessThan: "アカウント作ってから~以内" createdMoreThan: "アカウント作ってから~経過" followersLessThanOrEq: "フォロワー数が~以下" @@ -1750,7 +1656,6 @@ _emailUnavailable: disposable: "ずーっと使えるアドレスじゃないみたいや" mx: "正しいメールサーバーじゃないっぽいわ" smtp: "メールサーバーがうんともすんとも言わへん" - banned: "このメールアドレスはあかん" _ffVisibility: public: "公開" followers: "フォロワーだけに公開" @@ -1794,7 +1699,6 @@ _plugin: installWarn: "信頼できへんプラグインはインストールせんとってな" manage: "プラグインの管理" viewSource: "ソース見る" - viewLog: "ログを表示" _preferencesBackups: list: "作ったバックアップ" saveNew: "新しく保存" @@ -1809,8 +1713,8 @@ _preferencesBackups: deleteConfirm: "{name}を消すん?" renameConfirm: "「{old}」を「{new}」に変えるん?" noBackups: "バックアップはないで。「新しく保存」ってとこでこのクライアント設定を鯖に保存できるで。" - createdAt: "作った日時: {date} {time}" - updatedAt: "更新日時: {date} {time}" + createdAt: "作った日時:{date}{time}" + updatedAt: "更新日時:{date}{time}" cannotLoad: "読み込みできへん..." invalidFile: "ファイル形式が違うで?" _registry: @@ -1824,8 +1728,6 @@ _aboutMisskey: contributors: "主な貢献者" allContributors: "全ての貢献者" source: "ソースコード" - original: "オリジナル" - thisIsModifiedVersion: "{name}はオリジナルのCherryPickをいじったバージョンをつこうてるで。" translation: "Misskeyを翻訳" donate: "Misskeyに寄付" morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰" @@ -2013,6 +1915,8 @@ _sfx: notification: "通知" chat: "チャット" chatBg: "チャット(バックグラウンド)" + antenna: "アンテナ受信" + channel: "チャンネル通知" reaction: "ツッコミ選んどるとき" _soundSettings: driveFile: "ドライブん中の音使う" @@ -2050,6 +1954,7 @@ _2fa: registerTOTP: "認証アプリの設定はじめる" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな~。" + step2Click: "QRコード押したら、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" step2Uri: "デスクトップアプリを使う時は次のURIを入れるで" step3Title: "確認コードを入れてーや" step3: "アプリに映っとる確認コード(トークン)を入れて終わりや。" @@ -2073,7 +1978,6 @@ _2fa: backupCodesDescription: "認証アプリが使用できんなった場合、以下のバックアップコードを使ってアカウントにアクセスできるで。これらのコードは必ず安全な場所に置いときや。各コードは一回だけ使用できるで。" backupCodeUsedWarning: "バックアップコードが使用されたで。認証アプリが使えなくなってるん場合、なるべく早く認証アプリを再設定しや。" backupCodesExhaustedWarning: "バックアップコードが全て使用されたで。認証アプリを利用できん場合、これ以上アカウントにアクセスできなくなるで。認証アプリを再登録しや。" - moreDetailedGuideHere: "詳細なガイドはこちら" _permissions: "read:account": "アカウントの情報を見るで" "write:account": "アカウントの情報を変更するで" @@ -2111,54 +2015,6 @@ _permissions: "write:flash": "Playを操作する" "read:flash-likes": "Playのええやん!を見る" "write:flash-likes": "Playのええやん!を見る" - "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": "ユーザーのIPアドレスを見る" - "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: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": "ユーザーのドライブの情報見る" - "read:admin:stream": "管理者用のWebsocket API使う" - "write:admin:ad": "広告いじる" - "read:admin:ad": "広告見る" - "write:invite-codes": "招待コード作る" - "read:invite-codes": "招待コード取得" - "write:clip-favorite": "クリップのいいねいじる" - "read:clip-favorite": "クリップのいいね見る" - "read:federation": "連合の情報取得" - "write:report-abuse": "違反報告" _auth: shareAccessTitle: "アプリへのアクセス許してやったらどうや" shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?" @@ -2172,10 +2028,10 @@ _auth: _antennaSources: all: "みんなのノート" homeTimeline: "フォローしとるユーザーのノート" - users: "選んだ一人か複数のユーザーのノート" + users: "選らんだ一人か複数のユーザーのノート" userList: "選んだリストのユーザーのノート" userGroup: "選んだグループのユーザーのノート" - userBlacklist: "選んだ一人か複数のユーザーを除いた全てのノート" + userBlacklist: "選んだ1人か複数のユーザーのノート" _weekday: sunday: "日曜日" monday: "月曜日" @@ -2281,7 +2137,6 @@ _profile: _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" - clips: "クリップ" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" @@ -2336,7 +2191,6 @@ _play: title: "タイトル" script: "スクリプト" summary: "説明" - visibilityDescription: "非公開に設定するとプロフィールに表示されへんくなるけど、URLを知っとる人は引き続きアクセスできるで。" _pages: newPage: "ページを作る" editPage: "ページの編集" @@ -2381,8 +2235,6 @@ _pages: section: "セクション" image: "画像" button: "ボタン" - dynamic: "動的ブロック" - dynamicDescription: "このブロックは廃止されとるで。今後は{play}を利用してや。" note: "ノート埋め込み" _note: id: "ノートID" @@ -2405,7 +2257,6 @@ _notification: pollEnded: "アンケートの結果が出たみたいや" newNote: "さらの投稿" unreadAntennaNote: "アンテナ {name}" - roleAssigned: "ロールが付与されたで" emptyPushNotificationMessage: "プッシュ通知の更新をしといたで" achievementEarned: "実績を獲得しとるで" testNotification: "通知テスト" @@ -2413,10 +2264,8 @@ _notification: sendTestNotification: "テスト通知を送信するで" notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで" reactedBySomeUsers: "{n}人がツッコんだで" - likedBySomeUsers: "{n}人がいいねしたで" renotedBySomeUsers: "{n}人がリノートしたで" followedBySomeUsers: "{n}人にフォローされたで" - flushNotification: "通知の履歴をリセットする" _types: all: "すべて" note: "あんたらの新規投稿" @@ -2430,7 +2279,6 @@ _notification: receiveFollowRequest: "フォロー許可してほしいみたいやで" followRequestAccepted: "フォローが受理されたで" groupInvited: "グループに招待されたで" - roleAssigned: "ロールが付与された" achievementEarned: "実績の獲得" app: "連携アプリからの通知や" _actions: @@ -2481,6 +2329,7 @@ _webhookSettings: createWebhook: "Webhookをつくる" name: "名前" secret: "シークレット" + events: "Webhookを投げるタイミング" active: "有効" _events: follow: "フォローしたとき~!" @@ -2490,12 +2339,6 @@ _webhookSettings: renote: "リノートされるとき~!" reaction: "ツッコまれたとき~!" mention: "メンションがあるとき~!" - deleteConfirm: "ほんまにWebhookをほかしてもええんか?" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "メール" - deleteConfirm: "通知先を削除してもええか?" _moderationLogTypes: createRole: "ロールを追加すんで" deleteRole: "ロールほかす" @@ -2520,7 +2363,6 @@ _moderationLogTypes: resetPassword: "パスワードをリセット" suspendRemoteInstance: "リモートサーバーを止めんで" unsuspendRemoteInstance: "リモートサーバーを再開すんで" - updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新" markSensitiveDriveFile: "ファイルをセンシティブ付与" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" resolveAbuseReport: "苦情を解決" @@ -2595,72 +2437,3 @@ _dataSaver: _code: title: "コードハイライト" description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。" -_hemisphere: - N: "北半球" - S: "南半球" - caption: "一部のクライアント設定で、季節を判定するのに使用するで。" -_reversi: - reversi: "リバーシ" - gameSettings: "対局の設定" - chooseBoard: "ボードを選択" - blackOrWhite: "先行/後攻" - blackIs: "{name}が黒(先行)" - rules: "ルール" - thisGameIsStartedSoon: "対局、そろそろ開始されるで。" - waitingForOther: "相手の準備が完了するのを待ってんで。" - waitingForMe: "あんさんの準備が完了すんのを待ってんで" - waitingBoth: "準備してなー" - ready: "準備完了" - cancelReady: "準備を再開" - opponentTurn: "相手のターンやで" - myTurn: "あんさんのターンや" - turnOf: "{name}のターンやで" - pastTurnOf: "{name}のターン" - surrender: "投了" - surrendered: "投了により" - timeout: "時間切れ" - drawn: "引き分け" - won: "{name}の勝ち" - black: "黒" - white: "白" - total: "合計" - turnCount: "{count}ターン目" - myGames: "自分の対局" - allGames: "みんなの対局" - ended: "終了" - playing: "対局中" - isLlotheo: "石の少ない方が勝ち(ロセオ)" - loopedMap: "ループマップ" - canPutEverywhere: "どこでも置けるモード" - timeLimitForEachTurn: "1ターンの時間制限" - freeMatch: "フリーマッチ" - lookingForPlayer: "対戦相手を探してるで" - gameCanceled: "対局がキャンセルされたわ" - shareToTlTheGameWhenStart: "初めの時に対局をタイムラインに投稿するで" - iStartedAGame: "対局し始めたで! #MisskeyReversi" - opponentHasSettingsChanged: "相手が設定変えたで" - allowIrregularRules: "変則許可 (完全フリー)" - disallowIrregularRules: "変則なし" - showBoardLabels: "盤面に行・列番号を表示" - useAvatarAsStone: "石をアイコンにする" -_offlineScreen: - title: "オフライン - サーバーに接続できひんで" - header: "サーバーに接続できへんわ" -_urlPreviewSetting: - title: "URLプレビューの設定" - enable: "URLプレビューを有効にする" - timeout: "プレビュー取得時のタイムアウト(ms)" - timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されへんで。" - maximumContentLength: "Content-Lengthの最大値(byte)" - maximumContentLengthDescription: "Content-Lengthがこの値を超えた場合、プレビューは生成されへんで。" - requireContentLength: "Content-Lengthが取得できた場合のみプレビューを生成" - requireContentLengthDescription: "相手サーバがContent-Lengthを返さない場合、プレビューは生成されへんで。" - userAgent: "User-Agent" - userAgentDescription: "プレビュー取得時に使用されるUser-Agentを設定するで。空欄の場合、デフォルトのUser-Agentが使用されるで。" - summaryProxy: "プレビューを生成するプロキシのエンドポイント" - summaryProxyDescription: "CherryPick本体やなく、サマリープロキシを使用してプレビューを生成するで。" - summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されるで。プロキシ側がこれらをサポートせえへんときは、設定値は無視されるで。" -_mediaControls: - pip: "ピクチャインピクチャ" - playbackRate: "再生速度" - loop: "ループ再生" diff --git a/locales/jbo-EN.yml b/locales/jbo-EN.yml index 297ca53dd7..d4fea291d7 100644 --- a/locales/jbo-EN.yml +++ b/locales/jbo-EN.yml @@ -1,4 +1,3 @@ --- _lang_: "la .lojban." headlineMisskey: "lo se tcana noi jorne fi loi notci" - diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index d4aa36fa70..22e24d3baa 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -104,7 +104,3 @@ _deck: _columns: notifications: "Ilɣuyen" list: "Tibdarin" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Imayl" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index be45a6e866..756c41452d 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -84,4 +84,3 @@ _deck: notifications: "ಅಧಿಸೂಚನೆಗಳು" tl: "ಸಮಯಸಾಲು" mentions: "ಹೆಸರಿಸಿದ" - diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index b215f89293..114fe50c05 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -16,8 +16,8 @@ cancel: "아이예" noThankYou: "뎃어예" enterUsername: "사용자 이럼 서기" renotedBy: "{user}님이 리노트햇어예" -noNotes: "노트가 어ᇝ십니다" -noNotifications: "알림이 어ᇝ십니다" +noNotes: "노트가 없십니다" +noNotifications: "알림이 없십니다" instance: "서버" settings: "설정" notificationSettings: "알림 설정" @@ -26,7 +26,7 @@ otherSettings: "다린 설정" openInWindow: "창서 옐기" profile: "프로필" timeline: "타임라인" -noAccountDescription: "자기소개가 어ᇝ십니다" +noAccountDescription: "자기소개가 없십니다" login: "로그인" loggingIn: "로그인하고 잇어예" logout: "로그아웃" @@ -40,7 +40,7 @@ favorites: "질겨찾기" unfavorite: "질겨찾기서 어ᇝ애기" favorited: "질겨찾기에 담앗십니다." alreadyFavorited: "벌시로 질겨찾기에 담기 잇십니다." -cantFavorite: "질겨찾기에 몬 담앗십니다." +cantFavorite: "질겨찾기에 몬 담았십니다." pin: "프로필에 붙이기" unpin: "프로필서 띠기" copyContent: "내용 복사하기" @@ -80,7 +80,7 @@ unfollowConfirm: "{name}님얼 고마 팔로잉합니꺼?" exportRequested: "내가기 요청얼 햇십니다. 시간이 쪼매 걸릴 깁니다. 요청이 껕나모 ‘드라이브’에 옇십니다." importRequested: "가오기 요청얼 햇십니다. 시간이 쪼매 걸릴 깁니다." lists: "리스트" -noLists: "리스트가 어ᇝ십니다" +noLists: "리스트가 없십니다" note: "노트" notes: "노트" following: "팔로잉" @@ -124,7 +124,6 @@ reactions: "반엉" reactionSettingDescription2: "꺼시서 두고, 누질라서 뭉캐고, ‘+’럴 누질라서 옇십니다." rememberNoteVisibility: "공개 범위럴 기억하기" attachCancel: "붙임 빼기" -deleteFile: "파일 뭉캐기" markAsSensitive: "수ᇚ힘 설정" unmarkAsSensitive: "수ᇚ힘 무루기" enterFileName: "파일 이럼 서기" @@ -161,7 +160,7 @@ youCanCleanRemoteFilesCache: "파일 간리으 🗑️ 모냥얼 누질리모 cacheRemoteSensitiveFiles: "웬겍으 수ᇚ힌 파일얼 캐시하기" cacheRemoteSensitiveFilesDescription: "요 설정얼 꺼모 웬겍 수ᇚ힌 파일이 캐시하지 아이하고 바리 링크합니다." flagAsBot: "자동 게정입니다" -flagAsBotDescription: "요 게정얼 프로그램서 설라먼 키야 합니다. 키모 다런 개발자가 반엉얼 끋어ᇝ이 데풀이하지 몬 하게 도아 줄 수 잇고 CherryPick으 시스템서 자동 게정이 뎁니다." +flagAsBotDescription: "요 게정얼 프로그램서 설라먼 키야 합니다. 키모 다런 개발자가 반엉얼 끋없이 데풀이하지 몬 하게 도아 줄 수 잇고 CherryPick으 시스템서 자동 게정이 뎁니다." flagAsCat: "애웅애웅애웅애웅!" flagAsCatDescription: "애옹?" flagShowTimelineReplies: "타임라인서 노트으 답하기 보기" @@ -176,7 +175,7 @@ wallpaper: "벡지" setWallpaper: "벡지 설정" removeWallpaper: "벡지 뭉캐기" searchWith: "찾기: {q}" -youHaveNoLists: "리스트가 어ᇝ십니다" +youHaveNoLists: "리스트가 없십니다" followConfirm: "{name}님얼 팔로잉합니꺼?" proxyAccount: "프락시 게정" proxyAccountDescription: "프락시 게정언 턱벨한 조겐서 웬겍 팔로잉얼 하넌 게정입니다. 사용자가 웬겍 사용자럴 리스트에 옇얼 때 리스트에 옇언 사용자럴 누도 팔로잉 아이하모 할동이 서버로 아이 오니께 요 게정이 아인 프락시 게정얼 팔로잉하게 합니다." @@ -210,17 +209,17 @@ instanceInfo: "서버 정보" statistics: "통게" clearQueue: "대기옐 비우기" clearQueueConfirmTitle: "대기옐얼 비웁니꺼?" -clearQueueConfirmText: "대기옐에 잇넌 걸얼 아이 보냅니다. 흐이 요 동작언 할 필요가 어ᇝ십니다." +clearQueueConfirmText: "대기옐에 잇넌 걸얼 아이 보냅니다. 흐이 요 동작언 할 필요가 없십니다." clearCachedFiles: "캐시 비우기" clearCachedFilesConfirm: "캐시한 웬겍 파일얼 말캉 뭉캡니꺼?" blockedInstances: "차단한 서버" blockedInstancesDescription: "차단할라넌 서버으 호스트럴 줄 바꿈해서로 비이 줍니다. 차단한 서버넌 요 서버하고 교류 몬 합니다." silencedInstances: "수ᇚ훈 서버" -silencedInstancesDescription: "수ᇚ훌라넌 서버으 호스트럴 줄 바꿈해서로 비이 줍니다. 수ᇚ훈 서버으 게정언 말캉 ‘수ᇚ후기’가 데서 팔로잉 요청만 데고 팔로워가 아인 로컬 게정서 멘션얼 몬 합니다. 차단한 서버넌 상간 어ᇝ십니다." +silencedInstancesDescription: "수ᇚ훌라넌 서버으 호스트럴 줄 바꿈해서로 비이 줍니다. 수ᇚ훈 서버으 게정언 말캉 ‘수ᇚ후기’가 데서 팔로잉 요청만 데고 팔로워가 아인 로컬 게정서 멘션얼 몬 합니다. 차단한 서버넌 상간 없십니다." muteAndBlock: "수ᇚ훔하고 차단" mutedUsers: "수ᇚ훈 사용자" blockedUsers: "차단한 사용자" -noUsers: "사용자가 어ᇝ십니다" +noUsers: "사용자가 없십니다" editProfile: "프로필 적기" noteDeleteConfirm: "요 노트럴 뭉캡니꺼?" pinLimitExceeded: "더 몬 붙입니다" @@ -230,15 +229,15 @@ processing: "처리하고 잇어예" preview: "미리보기" default: "기본값" defaultValueIs: "기본값: {value}" -noCustomEmojis: "이모지가 어ᇝ십니다" -noJobs: "작업이 어ᇝ십니다" +noCustomEmojis: "이모지가 없십니다" +noJobs: "작업이 없십니다" federating: "옌합하고 잇어예" blocked: "차단햇어예" suspended: "고만 보내예" all: "말캉" subscribing: "구독하고 잇어예" publishing: "보내고 잇어예" -notResponding: "답이 어ᇝ어예" +notResponding: "답이 없어예" instanceFollowing: "서버으 팔로잉" instanceFollowers: "서버으 팔로워" instanceUsers: "서버으 사용자" @@ -275,7 +274,7 @@ uploadFromUrlRequested: "올리기럴 요청햇십니다" uploadFromUrlMayTakeTime: "올리기가 껕날라먼 시간이 쪼매 걸릴 깁니다." explore: "살펴보기" messageRead: "이럿어예" -noMoreHistory: "요카마 옛날 기록이 어ᇝ십니다" +noMoreHistory: "요카마 엣날 기록이 없십니다" startMessaging: "대화하기" nUsersRead: "{n}멩이 이럿십니다" agreeTo: "{0}에 동이하기" @@ -374,8 +373,6 @@ hcaptcha: "에이치캡차" enableHcaptcha: "에이치캡차 키기" hcaptchaSiteKey: "사이트키" hcaptchaSecretKey: "시크릿키" -mcaptchaSiteKey: "사이트키" -mcaptchaSecretKey: "시크릿키" recaptcha: "리캡차" enableRecaptcha: "리캡차 키기" recaptchaSiteKey: "사이트키" @@ -432,28 +429,28 @@ securityKey: "보안키" lastUsed: "마지막 쓰임" lastUsedAt: "마지막 쓰임: {t}" unregister: "맨걸기 무루기" -passwordLessLogin: "비밀번호 어ᇝ이 로그인" -passwordLessLoginDescription: "비밀번호 어ᇝ이 보안 키나 패스 키만 서서 로그인합니다." +passwordLessLogin: "비밀번호 없시 로그인" +passwordLessLoginDescription: "비밀번호 말고 보안키나 패스키 같은 것만 써 가 로그인합니다." resetPassword: "비밀번호 재설정" -newPasswordIs: "새 비밀번호넌 ‘{password}’입니다" +newPasswordIs: "새 비밀번호는 \"{password}\" 입니다" reduceUiAnimation: "화면 움직임 효과들을 수ᇚ후기" share: "노누기" notFound: "몬 찾앗십니다" -notFoundDescription: "선 주소에 맞넌 페이지가 어ᇝ십니다." -uploadFolder: "기본 올리기 위치" -markAsReadAllNotifications: "모던 알림얼 읽엄 포시" -markAsReadAllUnreadNotes: "모던 걸얼 읽엄 포시" -markAsReadAllTalkMessages: "모던 대화 읽엄 포시" +notFoundDescription: "고런 주소로 들어가는 하멘은 없십니다." +uploadFolder: "기본 업로드 위치" +markAsReadAllNotifications: "모든 알림 이럿다고 표시" +markAsReadAllUnreadNotes: "모든 글 이럿다고 표시" +markAsReadAllTalkMessages: "모든 대화 이럿다고 표시" help: "도움말" -inputMessageHere: "옇다 메시지럴 서이소" -close: "꺼기" +inputMessageHere: "여따가 메시지를 입력해주이소" +close: "닫기" invites: "초대하기" -members: "구성원" -transfer: "넘구기" +members: "멤버" +transfer: "양도" title: "제목" -text: "걸" +text: "글" enable: "키기" -next: "다엄" +next: "다음" retype: "다시 서기" noteOf: "{user}님으 노트" quoteAttached: "따옴" @@ -464,11 +461,8 @@ onlyOneFileCanBeAttached: "메시지엔 파일 하나까제밖에 몬 넣십니 invitations: "초대하기" invitationCode: "초대장" checking: "학인하고 잇십니다" -tooShort: "억수로 짜립니다" -tooLong: "억수로 집니다" passwordMatched: "맞십니다" passwordNotMatched: "안 맞십니다" -signinWith: "{n}서 로그인" signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소." or: "아니면" language: "언어" @@ -513,13 +507,13 @@ useObjectStorage: "오브젝트 스토리지 키기" objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "오브젝트 (미디어) 참조 링크 만들 때 쓰는 URL임다. CDN 내지 프락시를 쓴다 카멘은 그 URL을 갖다 늫고, 아이면 써먹을 서비스네 가이드를 봐봐가 공개적으로 접근할 수 있는 주소를 여 넣어 주이소. 그니께, 내가 AWS S3을 쓴다 카면은 'https://.s3.amazonaws.com', GCS를 쓴다 카면 'https://storage.googleapis.com/' 처럼 쓰믄 되입니더." objectStorageBucket: "Bucket" -objectStorageBucketDesc: "설 서비스으 버킷 이럼얼 서 주이소." +objectStorageBucketDesc: "써먹을 서비스의 바께쓰 이름을 여 써 주이소." objectStoragePrefix: "Prefix" objectStoragePrefixDesc: "요 Prefix 디렉토리 안에다가 파일이 들어감다." objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "AWS S3넌 비아 두고 다런 것언 거 서비스으 엔드포인트럴 서 주이소. ‘’나 ‘:’맨치로 섭니다." +objectStorageEndpointDesc: "AWS S3을 쓸라멘 요는 비워두고, 아이멘은 그 서비스 가이드에 맞게 endpoint를 넣어 주이소. '' 내지 ':'처럼 넣십니다." objectStorageRegion: "Region" -objectStorageRegionDesc: "‘xx-east-1’맨치로 리전 이럼얼 서 주이소. 설 서비스에 리전 개넴이 어ᇝ어먼 ‘us-east-1’라고 해 두이소. 에이더블유에스 설정 파일이나 환겡 벤수가 이ᇇ어면 비아 두이소." +objectStorageRegionDesc: "'xx-east-1' 같은 region 이름을 옇어 주이소. 써먹을 서비스에 region 개념 같은 게 읎다! 카면은 대신에 'us-east-1'을 옇어 놓으이소. AWS 설정 파일이나 환경 변수를 갖다 끌어다 쓸 거면은 요는 비워 두이소." objectStorageUseSSL: "SSL 쓰기" objectStorageUseSSLDesc: "API 호출할 때 HTTPS 안 쓸거면은 꺼 두이소" objectStorageUseProxy: "연결에 프락시 사용" @@ -535,21 +529,21 @@ newNoteRecived: "새 노트 있어예" sounds: "소리" sound: "소리" listen: "듣기" -none: "어ᇝ엄" +none: "없음" showInPage: "바닥서 보기" popout: "새 창 열기" volume: "음량" masterVolume: "대빵 음량" notUseSound: "음소거하기" -useSoundOnlyWhenActive: "CherryPick이 활성화되어 있을 때만 소리 내기" -details: "자세히" -chooseEmoji: "이모지 개리기" +useSoundOnlyWhenActive: "CherryPick가 활성화되어 있을 때만 소리 내기" +details: "좀 더" +chooseEmoji: "이모지 선택" unableToProcess: "작업 다 몬 했십니다" recentUsed: "최근 쓴 놈" install: "설치" uninstall: "삭제" installedApps: "설치된 애플리케이션" -nothing: "어ᇝ어예" +nothing: "뭣도 없어예" installedDate: "설치한 날" lastUsedDate: "마지막 사용" state: "상태" @@ -575,12 +569,7 @@ userSilenced: "요 게정은... 수ᇚ혀 있십니다." relays: "릴레이" addRelay: "릴레이 옇기" addedRelays: "옇은 릴레이" -deletedNote: "뭉캔 걸" enableInfiniteScroll: "알아서 더 보기" -useCw: "내용 수ᇚ후기" -description: "설멩" -describeFile: "캡션 옇기" -enterFileDescription: "캡션 서기" author: "맨던 사람" manage: "간리" emailServer: "전자우펜 서버" @@ -600,7 +589,6 @@ reporter: "신고한 사람" reporteeOrigin: "신고덴 사람" reporterOrigin: "신고한 곳" forwardReport: "웬겍 서버에 신고 보내기" -waitingFor: "{x}(얼)럴 지달리고 잇십니다" random: "무작이" system: "시스템" clip: "클립 맨걸기" @@ -610,19 +598,14 @@ renotesCount: "리노트한 수" renotedCount: "리노트덴 수" followingCount: "팔로우 수" followersCount: "팔로워 수" -noteFavoritesCount: "질겨찾기한 노트 수" clips: "클립 맨걸기" clearCache: "캐시 비우기" -typingUsers: "{users} 님이 서고 잇어예" unlikeConfirm: "좋네예럴 무룹니꺼?" info: "정보" -selectAccount: "계정 개리기" user: "사용자" administration: "간리" -translatedFrom: "{x}서 번옉" on: "킴" off: "껌" -hide: "수ᇚ후기" clickToFinishEmailVerification: "[{ok}]럴 누질라서 전자우펜 정멩얼 껕내이소." searchByGoogle: "찾기" tenMinutes: "십 분" @@ -631,8 +614,6 @@ oneDay: "하리" oneWeek: "한 주" oneMonth: "한 달" file: "파일" -typeToConfirm: "게속할라먼 {x}럴 누질라 주이소" -pleaseSelect: "개리 주이소" tools: "도구" like: "좋네예!" unlike: "좋네예 무루기" @@ -640,19 +621,12 @@ numberOfLikes: "좋네예 수" show: "보기" roles: "옉할" role: "옉할" -noRole: "옉할이 어ᇝ십니다" +noRole: "옉할이 없십니다" thisPostMayBeAnnoyingCancel: "아이예" likeOnly: "좋네예마" -myClips: "내 클립" icon: "아바타" replies: "답하기" renotes: "리노트" -attach: "옇기" -surrender: "아이예" -_delivery: - stop: "고만 보내예" - _type: - none: "보내고 잇어예" _initialAccountSetting: startTutorial: "길라잡이 하기" _initialTutorial: @@ -665,52 +639,9 @@ _initialTutorial: title: "길라잡이가 껕낫십니다!🎉" _achievements: _types: - _notes1: - description: "첫 노트럴 섯어예" - _notes10: - description: "노트럴 10번 섰어예" - _notes100: - description: "노트럴 100번 섰어예" - _notes500: - description: "노트럴 500번 섰어예" - _notes1000: - description: "노트럴 1,000번 섰어예" - _notes5000: - description: "노트럴 5,000번 섰어예" - _notes10000: - description: "노트럴 10,000번 섰어예" - _notes20000: - description: "노트럴 20,000번 섰어예" - _notes30000: - description: "노트럴 30,000번 섰어예" - _notes40000: - description: "노트럴 40,000번 섰어예" - _notes50000: - description: "노트럴 50,000번 섰어예" - _notes60000: - description: "노트럴 60,000번 섰어예" - _notes70000: - description: "노트럴 70,000번 섰어예" - _notes80000: - description: "노트럴 80,000번 섰어예" - _notes90000: - description: "노트럴 90,000번 섰어예" - _notes100000: - description: "노트럴 100,000번 섰어예" - _noteClipped1: - description: "첫 노트럴 클립햇어예" - _noteFavorited1: - description: "첫 노트럴 질겨찾기에 담앗어예" - _myNoteFavorited1: - description: "다런 사람이 내 노트럴 질겨찾기에 담앗십니다" - _iLoveCherryPick: - description: "“I ❤ #CherryPick”럴 섰어예" - _postedAt0min0sec: - description: "0분 0초에 노트를 섰어예" _tutorialCompleted: description: "길라잡이럴 껕냇십니다" _gallery: - my: "내 걸" liked: "좋네예한 걸" like: "좋네예!" unlike: "좋네예 무루기" @@ -721,24 +652,15 @@ _serverDisconnectedBehavior: reload: "알아서 새로곤침" _channel: removeBanner: "배너 뭉캐기" - usersCount: "{n}명 참여" - notesCount: "노트 {n}개" -_menuDisplay: - hide: "수ᇚ후기" _theme: - description: "설멩" keys: mention: "멘션" _sfx: note: "새 노트" notification: "알림" - reaction: "리액션 개리기" _2fa: step3Title: "학인 기호럴 서기" renewTOTPCancel: "뎃어예" -_permissions: - "read:favorites": "질겨찾기 보기" - "write:favorites": "질겨찾기 곤치기" _widgets: profile: "프로필" instanceInfo: "서버 정보" @@ -750,22 +672,14 @@ _widgets: _userList: chooseList: "리스트 개리기" _cw: - hide: "수ᇚ후기" show: "더 볼래예" - chars: "걸자 {count}개" - files: "파일 {count}개" _visibility: home: "덜머리" followers: "팔로워" -_postForm: - _placeholders: - e: "옇다 서 주이소" _profile: name: "이럼" username: "사용자 이럼" _exportOrImport: - favoritedNotes: "질겨찾기한 노트" - clips: "클립 맨걸기" followingList: "팔로잉" muteList: "수ᇚ후기" blockingList: "차단하기" @@ -775,20 +689,16 @@ _charts: _timelines: home: "덜머리" _play: - my: "내 플레이" script: "스크립트" - summary: "설멩" _pages: like: "좋네예" unlike: "좋네예 무루기" - my: "내 페이지" blocks: image: "이미지" _note: id: "노트 아이디" _notification: youWereFollowed: "새 팔로워가 잇십니다" - newNote: "새 걸" _types: follow: "팔로잉" mention: "멘션" @@ -805,19 +715,9 @@ _deck: mentions: "받언 멘션" _webhookSettings: name: "이럼" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "전자우펜" _moderationLogTypes: suspend: "얼우기" deleteNote: "노트 뭉캐기" deleteUserAnnouncement: "사용자 공지 걸 뭉캐기" resetPassword: "비밀번호 재설정" resolveAbuseReport: "신고 해겔하기" -_reversi: - reversi: "리버시" - chooseBoard: "보드 개리기" - black: "꺼멍" - white: "허영" - total: "합게" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 3dc14a532c..a4faf8b383 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1,6 +1,5 @@ --- _lang_: "한국어" -forceRenoteVisibilitySelector: "리노트 공개 범위 지정" cherrypickLabs: "CherryPick 실험실" cherrypickLabsDescription: "개발 중인 기능을 사용해 보시겠어요? 아직 개발 중인 기능이므로 제대로 작동하지 않을 수 있어요." copiedLink: "링크를 복사했어요!" @@ -17,7 +16,7 @@ opacity: "불투명도" noteUpdatedAt: "편집됨: {date} {time}" editReaction: "리액션 편집" removeReaction: "리액션 삭제" -noNyaization: "고양이체로 표시하지 않기" +noNyaization: "고양이체를 표시하지 않기" revertNoNyaization: "고양이체를 포함하여 표시" viewTextSource: "텍스트 소스 보기" disableNoteEditConfirm: "노트 편집을 계속 진행할까요?" @@ -113,9 +112,9 @@ addUser: "사용자 추가" favorite: "즐겨찾기" favorites: "즐겨찾기" unfavorite: "즐겨찾기에서 제거" -favorited: "즐겨찾기에 등록했어요." -alreadyFavorited: "이미 즐겨찾기에 등록되어 있어요." -cantFavorite: "즐겨찾기에 등록하지 못했어요." +favorited: "즐겨찾기에 등록했어요" +alreadyFavorited: "이미 즐겨찾기에 등록되어 있어요" +cantFavorite: "즐겨찾기에 등록하지 못했어요" pin: "프로필에 고정" unpin: "프로필에서 고정 해제" copyContent: "내용 복사" @@ -129,7 +128,7 @@ copyAndEditConfirm: "이 노트를 복사하고 편집할까요? 노트에 포 addToList: "리스트에 추가" addToAntenna: "안테나에 추가" sendMessage: "메시지 보내기" -copyRSS: "RSS 주소 복사" +copyRSS: "RSS 복사" copyUsername: "사용자 이름 복사" copyUserId: "사용자 ID 복사" copyNoteId: "노트 ID 복사" @@ -147,7 +146,7 @@ followRequestAccepted: "팔로우가 수락되었어요" mention: "멘션" mentions: "받은 멘션" directNotes: "다이렉트 노트" -importAndExport: "가져오기 및 내보내기" +importAndExport: "가져오기와 내보내기" import: "가져오기" export: "내보내기" files: "파일" @@ -169,9 +168,9 @@ error: "오류" somethingHappened: "오류가 발생했어요" retry: "다시 시도" pageLoadError: "페이지를 불러오지 못했어요." -pageLoadErrorDescription: "네트워크 연결 문제 또는 브라우저 캐시로 인해 발생했을 가능성이 높아요. 캐시를 삭제하거나, 잠시 후 다시 시도해 주세요." +pageLoadErrorDescription: "네트워크 연결 또는 브라우저 캐시로 인해 발생했을 가능성이 높아요. 캐시를 삭제하거나, 잠시 후 다시 시도해 주세요." serverIsDead: "서버에서 응답이 없어요. 잠시 후 다시 시도해 주세요." -youShouldUpgradeClient: "이 페이지를 보려면 페이지를 새로 고친 뒤, 새로운 버전의 클라이언트로 이용해 주세요." +youShouldUpgradeClient: "이 페이지를 표시하려면 페이지를 새로 고침해서 새로운 버전의 클라이언트를 이용해 주세요!" enterListName: "리스트 이름 입력" privacy: "프라이버시" makeFollowManuallyApprove: "팔로우를 수동으로 승인" @@ -185,7 +184,6 @@ enterEmoji: "이모지 입력" renote: "리노트" unrenote: "리노트 취소" renoted: "리노트했어요!" -renotedToX: "{name}명이 리노트했어요!" quoted: "노트를 인용했어요!" replied: "답글을 게시했어요!" cantRenote: "이 게시물은 리노트할 수 없어요." @@ -193,8 +191,6 @@ cantReRenote: "리노트된 노트는 리노트할 수 없어요." quote: "인용" inChannelRenote: "채널 내 리노트" inChannelQuote: "채널 내 인용" -renoteToChannel: "채널에 리노트" -renoteToOtherChannel: "다른 채널에 리노트" pinnedNote: "고정된 노트" pinned: "프로필에 고정" you: "나" @@ -205,21 +201,20 @@ add: "추가" reaction: "리액션" reactions: "리액션" emojiPicker: "이모지 선택기" -pinnedEmojisForReactionSettingDescription: "리액션을 할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있어요." -pinnedEmojisSettingDescription: "이모지를 입력할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있어요." +pinnedEmojisForReactionSettingDescription: "리액션을 할 때 프로필에 고정해서 표시할 이모지를 설정할 수 있어요" +pinnedEmojisSettingDescription: "이모지를 입력할 때 프로필에 고정해서 표시할 이모지를 설정할 수 있어요" emojiPickerDisplay: "선택기 표시" overwriteFromPinnedEmojisForReaction: "리액션 설정 덮어쓰기" overwriteFromPinnedEmojis: "일반 설정 덮어쓰기" reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있어요." rememberNoteVisibility: "공개 범위 기억하기" attachCancel: "첨부 취소" -deleteFile: "파일 삭제" markAsSensitive: "열람 주의로 설정" unmarkAsSensitive: "열람 주의 해제" enterFileName: "파일명 입력" mute: "뮤트" unmute: "뮤트 해제" -renoteMute: "리노트 뮤트" +renoteMute: "리노트 뮤트하기" renoteUnmute: "리노트 뮤트 해제" block: "차단" unblock: "차단 해제" @@ -252,21 +247,17 @@ cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모 flagAsBot: "삐릭, 삐리리릭? 저는 봇입니다." flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 봇 운영에 최적화되는 등의 변화가 생겨요" flagAsCat: "나는 고양이다냥" -flagAsCatDescription: "냥?(이 계정이 고양이라면 눌러 달라냥)" +flagAsCatDescription: "이 계정이 고양이라면 활성화해 달라냥" flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기" flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 사용자 간의 답글을 표시해요." autoAcceptFollowed: "팔로우 중인 사용자로부터의 팔로우 요청을 자동 수락" addAccount: "계정 추가" -reloadAccountsList: "계정 목록 새로고침" +reloadAccountsList: "계정 리스트 정보 갱신" loginFailed: "로그인에 실패했어요.." showOnRemote: "리모트에서 보기" -continueOnRemote: "리모트에서 계속하기" -chooseServerOnMisskeyHub: "Misskey Hub에서 서버 찾아보기" -specifyServerHost: "서버 도메인 직접 지정" -inputHostName: "도메인을 입력해 주세요" general: "일반" wallpaper: "배경" -setWallpaper: "배경 이미지 설정" +setWallpaper: "배경화면 설정" removeWallpaper: "배경 제거" searchWith: "검색: {q}" youHaveNoLists: "리스트가 없어요" @@ -307,7 +298,7 @@ clearQueueConfirmText: "아직 대기열에 남아 있는 노트는 연합우주 clearCachedFiles: "캐시 비우기" clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제할까요?" blockedInstances: "차단된 서버" -blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정해요. 차단된 서버는 이 서버와 통신할 수 없게 돼요." +blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정해요. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 돼요." silencedInstances: "사일런스한 서버" silencedInstancesDescription: "사일런스하려는 서버의 호스트명을 한 줄에 하나씩 입력해 주세요. 사일런스된 서버에 소속된 사용자는 모두 '사일런스'된 상태로 취급되며, 이 서버로부터의 팔로우가 프로필 설정과 무관하게 승인제로 변경되고, 팔로워가 아닌 로컬 사용자에게는 멘션할 수 없게 돼요. 정지된 서버에는 적용되지 않아요." muteAndBlock: "뮤트 및 차단" @@ -317,7 +308,7 @@ noUsers: "아무도 없어요" editProfile: "프로필 수정" noteDeleteConfirm: "이 노트를 삭제할까요?" pinLimitExceeded: "더 이상 고정할 수 없어요." -intro: "CherryPick의 설치를 완료했어요! 이제 관리자 계정을 만들 차례에요." +intro: "CherryPick의 설치가 완료되었어요! 이제 관리자 계정을 생성해주세요." done: "완료" processing: "처리 중" preview: "미리보기" @@ -366,7 +357,7 @@ uploadFromUrl: "URL 업로드" uploadFromUrlDescription: "업로드하려는 파일의 URL" uploadFromUrlRequested: "업로드를 요청했어요" uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 약간의 시간이 필요할 수 있어요." -explore: "둘러보기" +explore: "발견하기" messageRead: "읽음" messageSend: "보냄" noMoreHistory: "타임머신이 더 이상은 돌아갈 수 없대요!" @@ -379,7 +370,7 @@ basicNotesBeforeCreateAccount: "기본적인 주의사항" termsOfService: "이용 약관" start: "시작하기" home: "홈" -remoteUserCaution: "리모트 사용자에요! 이 서버와 정보가 일치하지 않을 수 있어요." +remoteUserCaution: "리모트 사용자에요! 인스턴스와 정보가 일치하지 않을 수 있어요." activity: "활동" images: "이미지" image: "이미지" @@ -401,7 +392,6 @@ selectFile: "파일 선택" selectFiles: "파일 선택" selectFolder: "폴더 선택" selectFolders: "폴더 선택" -fileNotSelected: "파일을 선택하지 않았어요." renameFile: "파일 이름 변경" folderName: "폴더 이름" createFolder: "폴더 만들기" @@ -469,11 +459,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptcha 활성화" hcaptchaSiteKey: "사이트 키" hcaptchaSecretKey: "시크릿 키" -mcaptcha: "mCaptcha" -enableMcaptcha: "mCaptcha 활성화" -mcaptchaSiteKey: "사이트 키" -mcaptchaSecretKey: "시크릿 키" -mcaptchaInstanceUrl: "mCaptcha 인스턴스 URL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA 활성화" recaptchaSiteKey: "사이트 키" @@ -489,7 +474,6 @@ name: "이름" antennaSource: "받을 소스" antennaKeywords: "받을 키워드" antennaExcludeKeywords: "제외할 키워드" -antennaExcludeBots: "봇 계정 제외" antennaKeywordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정할 수 있어요" notifyAntenna: "새로운 노트를 알림" withFileAntenna: "파일이 첨부된 노트만" @@ -526,13 +510,13 @@ moderationNote: "모더레이션 노트" addModerationNote: "모더레이션 노트 추가하기" moderationLogs: "모더레이션 로그" nUsersMentioned: "{n}명이 언급함" -securityKeyAndPasskey: "보안 키 또는 패스키" +securityKeyAndPasskey: "보안 키 또는 패스 키" securityKey: "보안 키" lastUsed: "마지막 사용" lastUsedAt: "마지막 사용: {t}" unregister: "등록 해제" passwordLessLogin: "비밀번호 없이 로그인" -passwordLessLoginDescription: "비밀번호 없이 보안 키 또는 패스키만 사용해서 로그인할 수 있어요." +passwordLessLoginDescription: "비밀번호를 사용하지 않고 보안 키 또는 패스 키 등으로만 로그인합니다." resetPassword: "비밀번호 재설정" newPasswordIs: "새로운 비밀번호는 \"{password}\" 에요!" reduceUiAnimation: "UI 애니메이션 줄이기" @@ -566,12 +550,10 @@ noteOf: "{user}의 노트" inviteToGroup: "그룹에 초대하기" quoteAttached: "인용함" quoteQuestion: "인용해서 작성하시겠어요?" -attachAsFileQuestion: "붙여 넣으려는 글이 너무 길어요. 텍스트 파일로 첨부할까요?" noMessagesYet: "너무.. 너무 조용해요.." newMessageExists: "새로운 메시지가 있어요!" onlyOneFileCanBeAttached: "메시지에는 하나의 파일만 첨부할 수 있어요!" -signinRequired: "진행하기 전에 먼저 로그인이 필요해요" -signinOrContinueOnRemote: "계속하려면 사용하는 계정이 있는 서버로 이동하거나 이 서버에 가입 또는 로그인해야 해요." +signinRequired: "로그인이 필요해요" invitations: "초대" invitationCode: "초대 코드" checking: "확인하고 있어요.." @@ -597,8 +579,7 @@ native: "네이티브" disableDrawer: "드로어 메뉴를 사용하지 않기" youHaveNoGroups: "그룹이 없어요" joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들 수 있어요!" -showNoteActionsOnlyHover: "노트에 커서를 올렸을 때에만 노트 동작 버튼 표시" -showReactionsCount: "노트의 리액션 수 표시하기" +showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시" noHistory: "기록이 없어요" signinHistory: "로그인 기록" enableAdvancedMfm: "고급 MFM 활성화" @@ -636,13 +617,13 @@ useObjectStorage: "오브젝트 스토리지 사용" objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "오브젝트 (미디어) 참조 URL을 만들 때 사용되는 URL이에요. CDN 또는 프록시를 사용하는 경우 그 URL을 지정하고, 그 외의 경우 사용할 서비스의 가이드에 따라 공개적으로 액세스 할 수 있는 주소를 지정해 주세요. 예를 들어, AWS S3의 경우 'https://.s3.amazonaws.com', GCS등의 경우 'https://storage.googleapis.com/' 와 같이 지정해 주세요." objectStorageBucket: "Bucket" -objectStorageBucketDesc: "사용하는 서비스의 bucket 이름을 지정해 주세요." +objectStorageBucketDesc: "사용 서비스의 bucket명을 지정해 주세요." objectStoragePrefix: "Prefix" objectStoragePrefixDesc: "이 Prefix 의 디렉토리 아래에 파일이 저장돼요." objectStorageEndpoint: "Endpoint" -objectStorageEndpointDesc: "AWS S3를 사용한다면 비워 두고 다른 서비스는 각 서비스의 endpoint를 설정해 주세요. ‘’ 혹은 ‘:’와 같이 지정해 주세요." +objectStorageEndpointDesc: "AWS S3의 경우 공란, 다른 서비스의 경우 각 서비스의 가이드에 맞게 endpoint를 설정해 주세요. '' 혹은 ':' 와 같이 지정해 주세요." objectStorageRegion: "Region" -objectStorageRegionDesc: "‘xx-east-1’처럼 region을 지정해 주세요. 사용하는 서비스에 region 개념이 없으면 ‘us-east-1’처럼 설정해 주세요. AWS 설정 파일이나 환경 변수가 있으면 비워 주세요." +objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해 주세요. 사용하는 서비스에 region 개념이 없는 경우 'us-east-1'으로 설정해 주세요. AWS 설정 파일 또는 환경 변수를 참조할 경우에는 비워주세요." objectStorageUseSSL: "SSL 사용" objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요" objectStorageUseProxy: "연결에 프록시 사용" @@ -668,7 +649,7 @@ showInPage: "페이지로 보기" popout: "새 창으로 열기" volume: "음량" masterVolume: "마스터 볼륨" -notUseSound: "모든 사운드 음소거" +notUseSound: "사운드 출력 비활성화" useSoundOnlyWhenActive: "CherryPick이 활성화된 경우에만 사운드 출력" details: "자세히" chooseEmoji: "이모지 선택" @@ -689,7 +670,7 @@ scratchpadDescription: "스크래치 패드는 AiScript 의 테스트 환경을 output: "출력" script: "스크립트" disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음" -updateRemoteUser: "원격 사용자 정보 갱신" +updateRemoteUser: "리모트 사용자 정보 갱신" unsetUserAvatar: "아바타 제거" unsetUserAvatarConfirm: "아바타를 제거할까요?" unsetUserBanner: "배너 제거" @@ -698,7 +679,7 @@ deleteAllFiles: "모든 파일 삭제" deleteAllFilesConfirm: "모든 파일을 삭제할까요?" removeAllFollowing: "모든 팔로잉 해제" removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요." -userSuspended: "이 사용자는 정지되었어요." +userSuspended: "이 계정은 정지된 상태에요." userSilenced: "이 계정은 사일런스된 상태에요." yourAccountSuspendedTitle: "계정이 정지되어 있어요" yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반했거나, 기타 다른 이유로 인해 정지되었어요. 자세한 사항은 관리자에게 문의해 주시고, 계정을 새로 생성하지 마세요." @@ -744,7 +725,6 @@ medium: "보통" small: "작게" generateAccessToken: "액세스 토큰 생성" permission: "권한" -adminPermission: "관리자 권한" enableAll: "전체 선택" disableAll: "전체 해제" tokenRequested: "계정 접근 허용" @@ -772,7 +752,7 @@ regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오 instanceMute: "서버 뮤트" userSaysSomething: "{name}님이 뭔가 말했어요!" makeActive: "활성화" -display: "보기" +display: "표시" copy: "복사" metrics: "통계" overview: "요약" @@ -788,7 +768,6 @@ useGlobalSettingDesc: "활성화하면 계정의 알림 설정이 적용돼요. other: "기타" regenerateLoginToken: "로그인 토큰 재생성" regenerateLoginTokenDescription: "이 작업은 로그인할 때 사용되는 내부 토큰을 다시 생성해요. 일반적으로 이 작업을 실행할 필요는 없지만, 다른 사람이 계정에 대한 접근 권한을 가지고 있다고 생각되거나, 공공장소에서 접속한 후 로그아웃하지 않았을 때 사용할 수 있어요. 이 기능을 사용하면 이 계정으로 로그인한 모든 기기에서 로그아웃 되고, 다시 로그인 해야해요." -theKeywordWhenSearchingForCustomEmoji: "커스텀 이모지를 검색할 때 키워드로 설정돼요." setMultipleBySeparatingWithSpace: "공백으로 구분해서 여러 개 설정할 수 있어요." fileIdOrUrl: "파일 ID 또는 URL" behavior: "동작" @@ -821,7 +800,7 @@ createNew: "새로 만들기" optional: "옵션" createNewClip: "새 클립 만들기" unclip: "클립 해제" -confirmToUnclipAlreadyClippedNote: "이 노트는 이미 ‘{name}’ 클립에 포함되어 있어요. 노트를 클립에서 제외할까요?" +confirmToUnclipAlreadyClippedNote: "이 노트는 이미 \"{name}\" 클립에 포함되어 있어요. 클립을 해제할까요?" public: "공개" private: "비공개" i18nInfo: "CherryPick은 자원봉사자들에 의해 다양한 언어로 번역되고 있어요. {link}에서 번역에 참가할 수 있어요." @@ -834,13 +813,13 @@ repliedCount: "받은 답글 수" renotedCount: "받은 리노트 수" followingCount: "팔로우 수" followersCount: "팔로워 수" -sentReactionsCount: "리액션 수" +sentReactionsCount: "보낸 리액션 수" receivedReactionsCount: "받은 리액션 수" -pollVotesCount: "투표 수" -pollVotedCount: "받은 투표 수" +pollVotesCount: "투표한 횟수" +pollVotedCount: "투표받은 횟수" yes: "예" no: "아니오" -driveFilesCount: "드라이브에 있는 파일 수" +driveFilesCount: "드라이브 파일 개수" driveUsage: "드라이브 사용량" noCrawle: "검색엔진의 인덱싱 거부" noCrawleDescription: "검색엔진이 사용자 페이지, 노트, 페이지 등의 콘텐츠를 인덱싱하지 못하게 해요." @@ -944,7 +923,6 @@ administration: "관리" accounts: "계정" switch: "전환" noMaintainerInformationWarning: "관리자 정보를 아직 설정하지 않았어요." -noInquiryUrlWarning: "문의처 주소를 아직 설정하지 않았어요." noBotProtectionWarning: "봇 방어가 설정되지 않았어요." configure: "설정하기" postToGallery: "갤러리에 업로드" @@ -1109,7 +1087,6 @@ neverShow: "다시 보지 않기" remindMeLater: "나중에 알림" didYouLikeMisskey: "CherryPick이 마음에 드시나요?" pleaseDonate: "CherryPick은 {host}에서 사용하는 무료 소프트웨어에요. 후원을 통해 앞으로도 개발을 계속할 수 있게 도와주세요!" -correspondingSourceIsAvailable: "소스 코드는 {anchor} 에서 받으실 수 있어요." roles: "역할" role: "역할" noRole: "역할이 없어요" @@ -1137,13 +1114,12 @@ thisPostMayBeAnnoyingHome: "홈에 게시" thisPostMayBeAnnoyingCancel: "그만두기" thisPostMayBeAnnoyingIgnore: "이대로 게시" collapseRenotes: "이미 본 리노트를 간략화하기" -collapseRenotesDescription: "리액션이나 리노트한 노트를 접어서 표시해요." collapseDefault: "특정 MFM 구문이 포함된 노트 간략화하기" internalServerError: "내부 서버 오류" internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했어요." copyErrorInfo: "오류 정보 복사" joinThisServer: "이 서버에 가입" -exploreOtherServers: "다른 서버 찾기" +exploreOtherServers: "다른 서버 둘러보기" letsLookAtTimeline: "타임라인 구경하기" disableFederationConfirm: "정말로 연합을 비활성화 할까요?" disableFederationConfirmWarn: "연합을 끄더라도 게시물이 비공개로 전환되는 것은 아니예요. 대부분의 경우 연합을 비활성화할 필요가 없어요." @@ -1160,11 +1136,8 @@ nonSensitiveOnlyForLocalLikeOnlyForRemote: "민감한 이모지를 제외하고 rolesAssignedToMe: "나에게 할당된 역할" resetPasswordConfirm: "비밀번호를 재설정할까요?" sensitiveWords: "민감한 단어" -sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제해요. 줄 바꿈으로 구분하여 여러 개를 지정할 수 있어요." +sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제해요. 개행으로 구분하여 여러 개를 지정할 수 있어요." sensitiveWordsDescription2: "공백으로 구분하면 AND로 지정되고, 키워드를 슬래시로 둘러싸면 정규 표현식이 돼요." -prohibitedWords: "금지 단어" -prohibitedWordsDescription: "설정된 단어가 포함되는 노트를 작성하려고 하면 오류로 표시해요. 줄 바꿈으로 구분하여 여러 개를 지정할 수 있어요." -prohibitedWordsDescription2: "공백으로 구분하면 AND로 지정되고, 키워드를 슬래시로 둘러싸면 정규 표현식이 돼요." hiddenTags: "숨긴 해시태그" hiddenTagsDescription: "설정한 태그가 트렌드에 표시되지 않아요. 줄 바꿈으로 하나씩 나눠서 설정할 수 있어요." notesSearchNotAvailable: "노트 검색을 이용할 수 없어요." @@ -1183,8 +1156,6 @@ limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시" noteIdOrUrl: "노트 ID 및 URL" video: "동영상" videos: "동영상" -audio: "오디오" -audioFiles: "오디오 파일" dataSaver: "데이터 절약 모드" accountMigration: "계정 이동" accountMoved: "이 사용자는 다음 계정으로 이사했어요:" @@ -1214,10 +1185,10 @@ continue: "계속" preservedUsernames: "예약된 사용자 이름" preservedUsernamesDescription: "예약할 사용자 이름을 한 줄에 하나씩 입력해 주세요. 여기에서 지정한 사용자 이름으로는 계정을 생성할 수 없게 돼요. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않아요." createNoteFromTheFile: "이 파일로 노트 작성" -archive: "보관" -channelArchiveConfirmTitle: "{name} 을(를) 보관할까요?" -channelArchiveConfirmDescription: "보관한 채널은 채널 목록과 검색 결과에 표시되지 않으며, 채널에 새로운 노트를 작성할 수 없게 돼요." -thisChannelArchived: "이 채널은 보관되었어요." +archive: "아카이브" +channelArchiveConfirmTitle: "{name} 을(를) 아카이브할까요?" +channelArchiveConfirmDescription: "아카이브한 채널은 채널 목록과 검색 결과에 표시되지 않으며, 채널에 새로운 노트를 작성할 수 없게 돼요." +thisChannelArchived: "이 채널은 아카이브 되었어요." displayOfNote: "노트 표시" initialAccountSetting: "초기 설정" youFollowing: "팔로잉" @@ -1286,7 +1257,6 @@ showRenotes: "리노트 표시" edited: "편집됨" notificationRecieveConfig: "알림 설정" mutualFollow: "맞팔로우" -followingOrFollower: "팔로 중이거나 팔로워" fileAttachedOnly: "미디어를 포함한 노트만" showRepliesToOthersInTimeline: "타임라인에 다른 사람에게 보내는 답글을 포함" hideRepliesToOthersInTimeline: "타임라인에 다른 사람에게 보내는 답글을 포함하지 않음" @@ -1295,13 +1265,6 @@ hideRepliesToOthersInTimelineAll: "타임라인에 팔로우 중인 모든 사 confirmShowRepliesAll: "이 조작은 되돌릴 수 없어요! 정말로 타임라인에 현재 팔로우 중인 모든 사람의 답글을 표시하도록 설정할까요?" confirmHideRepliesAll: "이 조작은 되돌릴 수 없어요! 정말로 타임라인에 현재 팔로우 중인 모든 사람의 답글을 표시하지 않도록 설정할까요?" externalServices: "외부 서비스" -sourceCode: "소스 코드" -sourceCodeIsNotYetProvided: "소스 코드를 아직 제공하지 않았어요. 이 문제를 해결하려면 관리자에게 문의해 주세요." -repositoryUrl: "저장소 URL" -repositoryUrlDescription: "소스 코드를 공개한 저장소가 있는 경우 그 URL을 적어주세요. CherryPick을 원본 그대로(소스 코드를 어떤 식으로도 변경하지 않고) 사용하는 경우, https://github.com/kokonect-link/cherrypick 라고 적어주세요." -repositoryUrlOrTarballRequired: "저장소를 공개하지 않는 경우, 대신 tarball을 제공해야 해요. 자세한 내용은 .config/example.yml을 참조해 주세요." -feedback: "피드백" -feedbackUrl: "피드백 URL" impressum: "운영자 정보" impressumUrl: "운영자 정보 URL" impressumDescription: "독일 등의 일부 나라와 지역에서는 꼭 표시해야 해요(Impressum)." @@ -1331,36 +1294,6 @@ seasonalScreenEffect: "계절에 따른 화면 연출" decorate: "장식하기" addMfmFunction: "장식 추가" enableQuickAddMfmFunction: "고급 MFM 선택기 표시하기" -bubbleGame: "버블 게임" -sfx: "효과음" -soundWillBePlayed: "사운드가 재생돼요" -showReplay: "리플레이 보기" -replay: "리플레이" -replaying: "리플레이 중" -endReplay: "리플레이 종료" -copyReplayData: "리플레이 데이터를 복사" -ranking: "랭킹" -lastNDays: "최근 {n}일" -backToTitle: "타이틀로 가기" -hemisphere: "거주 지역" -withSensitive: "민감한 파일이 포함된 노트 보기" -userSaysSomethingSensitive: "{name}님의 게시물에는 민감한 파일이 포함되어 있어요" -enableHorizontalSwipe: "밀어서 탭 전환" -loading: "불러오는 중" -surrender: "그만두기" -gameRetry: "다시 시도" -notUsePleaseLeaveBlank: "사용하지 않는 경우에는 비워두세요." -useTotp: "일회용 비밀번호 사용" -useBackupCode: "백업 코드 사용" -launchApp: "앱 실행" -useNativeUIForVideoAudioPlayer: "미디어 재생 시 브라우저 UI 사용" -keepOriginalFilename: "원본 파일 이름 유지" -keepOriginalFilenameDescription: "이 설정을 끄면, 파일을 업로드할 때 파일 이름이 무작위 문자열로 자동으로 변경돼요." -noDescription: "내용에 대한 설명이 없어요" -alwaysConfirmFollow: "팔로우일 때 항상 확인하기" -inquiry: "문의하기" -tryAgain: "다시 시도해 주세요." -confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 한 번 더 확인" showUnreadNotificationsCount: "읽지 않은 알림 수 표시" showCatOnly: "고양이만 보기" additionalPermissionsForFlash: "Play에 대한 추가 권한" @@ -1383,40 +1316,16 @@ _showingAnimatedImages: inactive: "일정 시간이 지나면 멈춤" _messaging: direct: "다이렉트 메시지" -_delivery: - status: "전송 상태" - stop: "정지됨" - resume: "전송 재개" - _type: - none: "배포 중" - manuallySuspended: "수동 정지 상태" - goneSuspended: "서버 삭제로 인한 정지 상태" - autoSuspendedForNotResponding: "서버가 응답하지 않아 일시적으로 정지됨" -_bubbleGame: - howToPlay: "플레이 방법" - hold: "홀드" - _score: - score: "점수" - scoreYen: "번 돈" - highScore: "최고 점수" - maxChain: "최대 콤보 수" - yen: "{yen}엔" - estimatedQty: "{qty}개" - scoreSweets: "오니기리 {onigiriQtyWithUnit}" - _howToPlay: - section1: "위치를 조정하여 상자에 물건을 떨어뜨려요." - section2: "같은 종류의 물건이 붙으면 다른 물건으로 바뀌면서 점수를 얻을 수 있어요." - section3: "상자에서 물건이 넘치면 게임 오버예요. 상자에서 물건이 넘치지 않도록 조심하면서 물건을 융합해 높은 점수를 획득하세요!" _announcement: forExistingUsers: "기존 사용자에게만 알리기" forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 사용자에게만 표시해요. 비활성화하면 게시 후에 가입한 사용자에게도 표시해요." needConfirmationToRead: "읽음으로 표시하기 전에 확인하기" needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 표시해요. '모두 읽음'의 대상에서도 제외돼요." end: "공지 내리기" - tooManyActiveAnnouncementDescription: "공지사항이 너무 많으면 사용자 경험에 영향을 끼칠 가능성이 있어요. 오래된 공지사항은 보관하는 것을 권장해요." + tooManyActiveAnnouncementDescription: "공지사항이 너무 많으면 사용자 경험에 영향을 끼칠 가능성이 있어요. 오래된 공지사항은 아카이브하는 것을 권장해요." readConfirmTitle: "읽음으로 표시할까요?" - readConfirmText: "〈{title}〉의 내용을 읽음으로 표시해요." - shouldNotBeUsedToPresentPermanentInfo: "신규 사용자의 이용 경험에 악영향을 끼칠 수 있으므로, 일시적인 알림 수단으로만 사용하고 고정된 정보에는 사용을 지양하는 것을 추천해요." + readConfirmText: "\"{title}\"을(를) 읽음으로 표시해요." + shouldNotBeUsedToPresentPermanentInfo: "신규 사용자의 사용자 경험에 악영향을 끼칠 수 있으므로, 일시적인 알림 수단으로만 사용하고 고정된 정보에는 사용을 지양하는 것을 추천해요." dialogAnnouncementUxWarn: "다이얼로그 형태의 알림이 동시에 2개 이상 존재하는 경우, 사용자 경험에 악영향을 끼칠 수 있으므로 신중히 결정하는 것을 권장드려요." silence: "조용히 알림" silenceDescription: "활성화하면 공지사항에 대한 알림이 가지 않게 되며, 확인 버튼을 누르지 않아도 공지사항이 읽음으로 표시돼요." @@ -1437,10 +1346,6 @@ _cherrypick: showRenoteConfirmPopupDescription: "이 설정은 '일반 - 리노트와 인용 버튼을 분리해서 표시하기' 설정이 켜져 있어야 해요." expandOnNoteClick: "노트를 클릭하여 자세히 표시" expandOnNoteClickDescription: "비활성화한 경우에도 노트 메뉴에서 '자세히'를 클릭하거나 타임스탬프를 클릭하여 열 수 있어요." - expandOnNoteClickBehavior: "노트를 클릭해서 열 때" - _expandOnNoteClickBehavior: - click: "클릭해서 열기" - doubleClick: "두 번 클릭해서 열기" displayHeaderNavBarWhenScroll: "스크롤 시 요소 표시 (헤더, 플로팅 버튼, 탐색 모음)" _displayHeaderNavBarWhenScroll: all: "모두 표시" @@ -1451,7 +1356,7 @@ _cherrypick: hide: "모두 숨기기" patch: "패치" patchDescription: "Misskey의 기능을 변경해요." - reactableRemoteReaction: "서버에 리모트 이모지와 이름이 같은 이모지가 있으면 리모트 이모지에도 리액션할 수 있음" + reactableRemoteReaction: "서버에 리모트 이모지와 이름이 같은 이모지가 있으면 리모트 이모지에도 반응할 수 있음" showFollowingMessageInsteadOfButton: "이미 팔로우한 경우 알림 필드에 팔로우 버튼을 표시하지 않음" mobileHeaderChange: "모바일 환경에서 헤더 디자인을 변경" renameTheButtonInPostFormToNya: "노트 작성 화면의 '노트' 버튼을 '냥!'으로 변경" @@ -1598,8 +1503,6 @@ _serverSettings: fanoutTimelineDescription: "활성화하면 각종 타임라인을 가져올 때의 성능을 대폭 향상하며, 데이터베이스의 부하를 줄일 수 있어요. 단, Redis의 메모리 사용량이 증가하게 되고, 서버의 메모리 용량이 작거나, 서비스가 불안정해지는 경우 해당 설정을 비활성화해 주세요." fanoutTimelineDbFallback: "데이터베이스 폴백" fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해서는 DB에 추가로 쿼리하는 폴백 처리를 수행해요. 비활성화하면 폴백 처리를 하지 않아 서버의 부하를 줄일 수 있지만, 타임라인을 가져올 수 있는 범위가 한정돼요." - inquiryUrl: "문의처 URL" - inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정할 수 있어요." _accountMigration: moveFrom: "다른 계정에서 이 계정으로 이사" moveFromSub: "다른 계정에 대한 별칭을 생성" @@ -1817,10 +1720,10 @@ _achievements: description: "3개 이상의 창을 열었어요" _driveFolderCircularReference: title: "순환 참조" - description: "드라이브 폴더에 스스로를 넣었어요" + description: "드라이브 폴더가 자신을 가리키도록 만드려고 시도했어요" _reactWithoutRead: title: "읽고 답하긴 하시는 건가요?" - description: "100자가 넘는 노트를 작성한 뒤 3초 안에 반응했어요" + description: "100자가 넘는 노트가 작성되고 3초 안에 반응했어요" _clickedClickHere: title: "여길 눌러보세요. 그리고 위를 쳐다보세요." description: "위를 보셨나요? 그렇다면 당신은 속으셨네요!" @@ -1863,13 +1766,6 @@ _achievements: _tutorialCompleted: title: "CherryPick 입문자 과정 수료증" description: "튜토리얼을 완료했어요. 어서오세요!" - _bubbleGameExplodingHead: - title: "🤯" - description: "버블 게임에서 가장 큰 물건을 내놓았어요" - _bubbleGameDoubleExplodingHead: - title: "더블🤯" - description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았어요" - flavor: "이 정도만 도시락통에 🤯🤯 조금만 더" _role: new: "새 역할 생성" edit: "역할 편집" @@ -1911,7 +1807,6 @@ _role: ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" canEditNote: "노트 편집 허용" - mentionMax: "노트에서 언급할 수 있는 멘션 수" canInvite: "서버 초대 코드 발행" inviteLimit: "초대 한도" inviteLimitCycle: "초대 발급 간격" @@ -1920,15 +1815,14 @@ _role: canManageAvatarDecorations: "아바타 장식 관리" driveCapacity: "드라이브 용량" alwaysMarkNsfw: "파일을 항상 NSFW로 지정" - canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용" - pinMax: "고정할 수 있는 최대 노트 수" - antennaMax: "만들 수 있는 최대 안테나 수" - wordMuteMax: "단어 뮤트할 수 있는 최대 문자 수" - webhookMax: "생성할 수 있는 최대 Webhook 수" - clipMax: "생성할 수 있는 최대 클립 수" - noteEachClipsMax: "각 클립에 추가할 수 있는 최대 노트 수" - userListMax: "생성할 수 있는 최대 사용자 리스트 수" - userEachUserListsMax: "사용자 리스트에 추가할 수 있는 최대 사용자 수" + pinMax: "고정할 수 있는 노트 수" + antennaMax: "최대 안테나 생성 허용 수" + wordMuteMax: "단어 뮤트할 수 있는 문자 수" + webhookMax: "생성할 수 있는 웹훅 수" + clipMax: "생성할 수 있는 클립 수" + noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수" + userListMax: "생성할 수 있는 사용자 리스트 수" + userEachUserListsMax: "사용자 리스트당 최대 사용자 수" rateLimitFactor: "요청 빈도 제한" descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화돼요." canHideAds: "광고 숨기기" @@ -1936,14 +1830,8 @@ _role: canUseTranslator: "번역 기능 이용 가능 여부" avatarDecorationLimit: "최대로 붙일 수 있는 아바타 장식 개수" _condition: - roleAssignedTo: "수동 역할에 이미 할당됨" isLocal: "로컬 사용자" isRemote: "리모트 사용자" - isCat: "계정을 고양이로 설정한 사용자" - isBot: "계정을 봇으로 설정한 사용자" - isSuspended: "정지된 사용자" - isLocked: "‘팔로우를 수동으로 승인’을 활성화한 사용자" - isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 사용자" createdLessThan: "가입한 지 다음 일수 이내인 사용자" createdMoreThan: "가입한 지 다음 일수 이상인 사용자" followersLessThanOrEq: "팔로워 수가 다음 이하인 사용자" @@ -2017,7 +1905,6 @@ _plugin: installWarn: "신뢰할 수 없는 플러그인은 설치하지 않는 것이 좋아요." manage: "플러그인 관리" viewSource: "소스 보기" - viewLog: "로그 보기" _preferencesBackups: list: "생성한 백업" saveNew: "새 백업 만들기" @@ -2028,10 +1915,10 @@ _preferencesBackups: cannotSave: "저장하지 못했어요" nameAlreadyExists: "\"{name}\" 백업이 이미 있어요. 다른 이름을 설정해 주세요." applyConfirm: "\"{name}\" 백업을 이 기기에 적용할까요? 현재 설정은 덮어쓰기 돼요!" - saveConfirm: "{name} 백업을 덮어쓸까요?" - deleteConfirm: "{name} 백업을 삭제할까요?" - renameConfirm: "‘{old}’ 백업의 이름을 ‘{new}’(으)로 바꿀까요?" - noBackups: "저장된 백업이 없어요. \"새 백업 만들기\"를 눌러 현재 클라이언트의 설정을 서버에 백업할 수 있어요!" + saveConfirm: "{name} 을 덮어쓸까요?" + deleteConfirm: "{name} 을(를) 삭제할까요?" + renameConfirm: "\"{old}\" 백업을 \"{new}\"(으)로 바꿀까요?" + noBackups: "저장된 백업이 없어요. \"새 백업 만들기\"를 눌러 이 클라이언트 설정을 서버에 백업할 수 있어요!" createdAt: "생성 날짜: {date} {time}" updatedAt: "갱신 날짜: {date} {time}" cannotLoad: "가져오기에 실패했어요" @@ -2047,8 +1934,6 @@ _aboutMisskey: contributors: "주요 기여자" allContributors: "모든 기여자" source: "소스 코드" - original: "원본" - thisIsModifiedVersion: "{name}에서는 원본 CherryPick을 수정해서 사용하고 있어요." translation: "Misskey를 번역하기" donate: "Misskey에 기부하기" morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰" @@ -2181,7 +2066,7 @@ _instanceMute: title: "지정한 서버의 노트가 숨겨져요." heading: "뮤트할 서버" _theme: - explore: "테마 둘러보기" + explore: "테마 찾아보기" install: "테마 설치" manage: "테마 관리" code: "테마 코드" @@ -2258,6 +2143,8 @@ _sfx: notification: "알림" chat: "대화" chatBg: "대화 (백그라운드)" + antenna: "안테나 수신" + channel: "채널 알림" reaction: "리액션 선택" _soundSettings: driveFile: "드라이브에 있는 오디오 파일 사용" @@ -2295,7 +2182,8 @@ _2fa: registerTOTP: "인증 앱 설정 시작" step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치해 주세요." step2: "인증 앱을 설치했다면, 표시되어 있는 QR 코드를 앱으로 스캔해 주세요." - step2Uri: "데스크톱 앱을 사용하려면 다음 URI를 입력해 주세요:" + step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있어요." + step2Uri: "데스크톱 앱을 사용하러면 다음 URI를 입력해 주세요:" step3Title: "인증 코드 입력" step3: "앱에 표시된 토큰을 입력하면 완료돼요!" setupCompleted: "설정을 완료했어요!" @@ -2318,7 +2206,6 @@ _2fa: backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다." backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오." backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요." - moreDetailedGuideHere: "여기에 자세한 설명이 있어요." _permissions: "read:account": "계정의 정보를 조회합니다" "write:account": "계정의 정보를 변경합니다" @@ -2369,6 +2256,7 @@ _permissions: "read:admin:server-info": "서버 정보 보기" "read:admin:show-moderation-log": "모더레이션 기록 보기" "read:admin:show-user": "사용자 개인정보 보기" + "read:admin:show-users": "사용자 개인정보 보기" "write:admin:suspend-user": "사용자 정지하기" "write:admin:unset-user-avatar": "사용자 아바타 삭제하기" "write:admin:unset-user-banner": "사용자 배너 삭제하기" @@ -2406,9 +2294,9 @@ _permissions: "write:report-abuse": "위반 내용 신고하기" _auth: shareAccessTitle: "애플리케이션 접근 허가" - shareAccess: "‘{name}’에서 계정에 접근하는 것을 허용할까요?" + shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용할까요?" shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용할까요?" - permission: "‘{name}’에서 다음 권한을 요청했어요" + permission: "{name}에서 다음 권한을 요청했어요" permissionAsk: "이 앱은 다음 권한을 요청하고 있습니다" pleaseGoBack: "앱으로 돌아가서 계속 진행해 주세요" callback: "앱으로 돌아갈게요!" @@ -2508,7 +2396,7 @@ _postForm: b: "무슨 일이 일어나고 있나요?" c: "무엇을 생각하고 있나요?" d: "말하고 싶은 게 있나요?" - e: "여기에 적어 주세요" + e: "여기에 적어주세요" f: "작성해주시길 기다리고 있어요..." _profile: name: "이름" @@ -2527,7 +2415,6 @@ _profile: _exportOrImport: allNotes: "모든 노트" favoritedNotes: "즐겨찾기한 노트" - clips: "클립" followingList: "팔로잉" muteList: "뮤트" blockingList: "차단" @@ -2551,15 +2438,15 @@ _charts: storageUsageTotal: "스토리지 사용량 합계" _instanceCharts: requests: "요청" - users: "사용자 수 변동" + users: "사용자 수 증감" usersTotal: "누적 사용자 수" - notes: "노트 수 변동" + notes: "노트 수 증감" notesTotal: "누적 노트 수" - ff: "팔로잉/팔로워 변동" + ff: "팔로잉/팔로워 증감" ffTotal: "누적 팔로잉/팔로워 수" - cacheSize: "캐시 용량 변동" + cacheSize: "캐시 용량 증감" cacheSizeTotal: "누적 캐시 용량" - files: "파일 수 변동" + files: "파일 수 증감" filesTotal: "누적 파일 수" _timelines: home: "홈" @@ -2581,7 +2468,6 @@ _play: title: "제목" script: "스크립트" summary: "설명" - visibilityDescription: "비공개로 설정하면 프로필에 표시하지 않지만 URL을 아는 사람은 계속해서 접속할 수 있습니다." _pages: newPage: "페이지 만들기" editPage: "페이지 편집" @@ -2626,8 +2512,6 @@ _pages: section: "섹션" image: "이미지" button: "버튼" - dynamic: "동적 블록" - dynamicDescription: "이 블록은 폐지되었습니다. 이제부터 {play}에서 이용해 주세요." note: "노트필기" _note: id: "노트 ID" @@ -2642,7 +2526,7 @@ _notification: youGotMention: "{name} 님이 나를 멘션했어요!" youGotReply: "{name} 님이 답글을 달았어요!" youGotQuote: "{name} 님이 인용했어요!" - youGotReact: "{name} 님이 리액션했어요!" + youGotReact: "{name} 님이 반응했어요!" youRenoted: "{name} 님이 리노트했어요!" youWereFollowed: "새로운 팔로워가 있어요!" youReceivedFollowRequest: "새로운 팔로우 요청이 있어요!" @@ -2658,11 +2542,9 @@ _notification: checkNotificationBehavior: "알림 표시 확인하기" sendTestNotification: "테스트 알림 보내기" notificationWillBeDisplayedLikeThis: "알림이 이렇게 표시돼요!" - reactedBySomeUsers: "{n}명이 리액션했어요" - likedBySomeUsers: "{n}명이 좋아요를 눌렀어요" + reactedBySomeUsers: "{n}명이 반응했어요" renotedBySomeUsers: "{n}명이 리노트했어요" followedBySomeUsers: "{n}명에게 팔로우됨" - flushNotification: "모든 알림 지우기" _types: all: "전부" note: "사용자의 새 게시물" @@ -2687,7 +2569,6 @@ _deck: alwaysShowMainColumn: "메인 칼럼 항상 표시" columnAlign: "칼럼 정렬" addColumn: "칼럼 추가" - newNoteNotificationSettings: "새 노트 알림 설정" configureColumn: "칼럼 설정" swapLeft: "왼쪽으로 이동" swapRight: "오른쪽으로 이동" @@ -2716,7 +2597,7 @@ _deck: direct: "다이렉트" roleTimeline: "역할 타임라인" _dialog: - charactersExceeded: "최대 글자수를 초과했어요! 현재 {current} / 최대 {max}" + charactersExceeded: "최대 글자수를 초과했어요! 현재 {current} / 최대 {min}" charactersBelow: "최소 글자수 미만이에요! 현재 {current} / 최소 {min}" _disabledTimeline: title: "비활성화된 타임라인" @@ -2726,9 +2607,9 @@ _drivecleaner: orderByCreatedAtAsc: "등록일이 오래된 순" _webhookSettings: createWebhook: "Webhook 생성" - modifyWebhook: "Webhook 수정" name: "이름" secret: "시크릿" + events: "Webhook을 실행할 타이밍" active: "활성화" _events: follow: "누군가를 팔로우 했을 때" @@ -2738,26 +2619,6 @@ _webhookSettings: renote: "누군가 내 글을 리노트 했을 때" reaction: "누군가 내 노트에 리액션 했을 때" mention: "누군가 나를 멘션 했을 때" - _systemEvents: - abuseReport: "사용자로부터 신고를 받았을 때" - abuseReportResolved: "받은 신고를 처리했을 때" - userCreated: "사용자가 생성되었을 때" - deleteConfirm: "이 Webhook을 삭제할까요?" -_abuseReport: - _notificationRecipient: - createRecipient: "신고 수신자 추가" - modifyRecipient: "신고 수신자 편집" - recipientType: "알림 수신 유형" - _recipientType: - mail: "이메일" - webhook: "Webhook" - _captions: - mail: "모더레이터 권한을 가진 사용자의 이메일 주소에 알림 보내기 (신고를 받았을 때만)" - webhook: "지정한 SystemWebhook으로 알림 보내기 (신고를 받았을 때와 해결했을 때 송신)" - keywords: "키워드" - notifiedUser: "신고 알림을 보낼 사용자" - notifiedWebhook: "사용할 Webhook" - deleteConfirm: "수신자를 삭제할까요?" _moderationLogTypes: createRole: "역할 생성" deleteRole: "역할 삭제" @@ -2782,10 +2643,9 @@ _moderationLogTypes: resetPassword: "비밀번호 재설정" suspendRemoteInstance: "리모트 서버를 정지" unsuspendRemoteInstance: "리모트 서버의 정지를 해제" - updateRemoteInstanceNote: "리모트 서버의 조정 기록 갱신" markSensitiveDriveFile: "파일을 열람 주의로 설정" unmarkSensitiveDriveFile: "파일의 열람 주의를 해제" - resolveAbuseReport: "신고 처리" + resolveAbuseReport: "신고 해결" createInvitation: "초대 코드 생성" createAd: "광고 생성" deleteAd: "광고 삭제" @@ -2795,12 +2655,6 @@ _moderationLogTypes: deleteAvatarDecoration: "아바타 장식 삭제" unsetUserAvatar: "이 사용자의 아바타 제거" unsetUserBanner: "이 사용자의 배너 제거" - createSystemWebhook: "SystemWebhook 생성" - updateSystemWebhook: "SystemWebhook 수정" - deleteSystemWebhook: "SystemWebhook 삭제" - createAbuseReportNotificationRecipient: "신고 알림 수신자 생성" - updateAbuseReportNotificationRecipient: "신고 알림 수신자 편집" - deleteAbuseReportNotificationRecipient: "신고 알림 수신자 삭제" _fileViewer: title: "파일 상세" type: "파일 유형" @@ -2863,76 +2717,7 @@ _dataSaver: description: "URL 미리보기의 썸네일 이미지를 불러오지 않아요." _code: title: "코드 문법 강조" - description: "MFM 등에서 코드 문법 강조 기법을 사용할 때, 클릭하기 전까지는 불러오지 않아요. 코드 문법 강조 기능은 강조할 언어마다 해당 정의 파일을 불러와야 하지만, 이를 자동으로 불러오지 않게 되어 데이터 사용량을 줄일 수 있어요." -_hemisphere: - N: "북반구" - S: "남반구" - caption: "일부 클라이언트 설정에서 계절을 설정할 때 사용돼요." -_reversi: - reversi: "리버시" - gameSettings: "대국 설정" - chooseBoard: "보드 선택" - blackOrWhite: "선공/후공" - blackIs: "{name}님이 흑(선공)" - rules: "규칙" - thisGameIsStartedSoon: "대국이 곧 시작돼요. 준비해 주세요" - waitingForOther: "상대방의 준비가 완료되기를 기다리고 있어요" - waitingForMe: "상대방이 나의 준비가 완료되기를 기다리고 있어요" - waitingBoth: "준비가 완료되면 '준비 완료'를 눌러주세요" - ready: "준비 완료" - cancelReady: "준비 취소" - opponentTurn: "상대방 차례에요" - myTurn: "내 차례에요" - turnOf: "{name}님의 차례에요" - pastTurnOf: "{name}님의 차례" - surrender: "기권(투료)" - surrendered: "기권(투료)에 의해" - timeout: "시간 초과" - drawn: "무승부" - won: "{name}님의 승리" - black: "흑" - white: "백" - total: "합계" - turnCount: "{count}턴째" - myGames: "내 대국" - allGames: "모든 대국" - ended: "종료" - playing: "대국 중" - isLlotheo: "돌이 적은 쪽이 승리(로세오)" - loopedMap: "반복 맵" - canPutEverywhere: "어디에나 둘 수 있는 모드" - timeLimitForEachTurn: "각 수의 시간 제한" - freeMatch: "자유 대국" - lookingForPlayer: "대국 상대를 찾고 있어요" - gameCanceled: "대국이 취소되었어요" - shareToTlTheGameWhenStart: "대국 시작 시 타임라인에 대국 게시하기" - iStartedAGame: "대국이 시작되었어요! #MisskeyReversi" - opponentHasSettingsChanged: "상대방이 게임 설정을 변경했어요" - allowIrregularRules: "규칙 변경 허용(완전 자유)" - disallowIrregularRules: "규칙 변경 없음" - showBoardLabels: "판에 행·열 번호 표시" - useAvatarAsStone: "돌을 아이콘으로 표시" -_offlineScreen: - title: "오프라인 - 서버에 연결할 수 없음" - header: "서버에 연결할 수 없어요" -_urlPreviewSetting: - title: "URL 미리보기 설정" - enable: "URL 미리보기 활성화" - timeout: "미리보기를 불러올 때의 타임아웃 (ms)" - timeoutDescription: "미리보기를 로딩하는데 걸리는 시간이 지정한 시간보다 오래 걸리면 미리보기를 생성하지 않아요." - maximumContentLength: "Content-Length의 최대치 (byte)" - maximumContentLengthDescription: "Content-Length가 이 값을 넘어서면 미리보기를 생성하지 않아요." - requireContentLength: "Content-Length를 반환했을 때만 미리보기 만들기" - requireContentLengthDescription: "원격 서버가 Content-Length를 반환하지 않는다면 미리보기를 만들지 않아요." - userAgent: "User-Agent" - userAgentDescription: "미리보기를 얻을 때 사용한 User-Agent를 설정해요. 비어 있다면 기본값의 User-Agent를 사용해요." - summaryProxy: "미리보기를 만든 프록시의 엔드포인트" - summaryProxyDescription: "CherryPick에서 미리보기를 만들지 않고 Summary Proxy를 경유해 미리보기를 만들어요." - summaryProxyDescription2: "프록시는 아래 변수를 쿼리 문자열로 연동해요. 프록시에서 이를 지원하지 않으면 설정값을 무시해요." -_mediaControls: - pip: "화면 속 화면" - playbackRate: "재생 속도" - loop: "반복 재생" + description: "MFM 등에서 코드 문법 강조 기법을 사용할 때, 탭하기 전까지는 불러오지 않아요. 코드 문법 강조 기능은 강조할 언어마다 해당 정의 파일을 불러와야 하지만, 이를 자동으로 불러오지 않게 되어 데이터 사용량을 줄일 수 있어요." _abuse: _resolver: 1hour: "1시간" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 275cdaa6b2..6bb11f0388 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -1,9 +1,9 @@ --- _lang_: "ພາສາລາວ" -headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍ note" -introMisskey: "ຍິນດີຕ້ອນຮັບ! CherryPick ເປັນຊອຟແວopensource, ສຳລັບບໍລິການ microblogging ແບບ decentralized\nສ້າງ “note” ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆ ຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nຢ່າລືມ “reaction” ໂນຕຂອງລາວເພື່ອສະແດງຄວາມຮູ້ສຶກ 👍\nມາສຳຫຼວດໂລກໃໝ່ແນ! 🚀" +headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍຫມາຍເຫດ" +introMisskey: "ຍິນດີຕ້ອນຮັບ! CherryPick ເປັນແຫຼ່ງເປີດ, ການບໍລິການ microblogging ກະຈາຍ\nສ້າງ \"ບັນທຶກ\" ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nດ້ວຍ \"ປະຕິກິລິຍາ\", ທ່ານຍັງສາມາດສະແດງຄວາມຮູ້ສຶກຂອງທ່ານຢ່າງໄວວາກ່ຽວກັບບັນທຶກຂອງທຸກໆຄົນ 👍\nມາສຳຫຼວດໂລກໃໝ່! 🚀" poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. CherryPick (ເອີ້ນວ່າ \"CherryPick instance\")" -monthAndDay: "ເດືອນ{month} / ວັນ{day}" +monthAndDay: "{ເດືອນ}/{ມື້}" search: "ຄົ້ນຫາ" notifications: "ການແຈ້ງເຕືອນ" username: "ຊື່ຜູ້ໃຊ້" @@ -15,79 +15,78 @@ gotIt: "ເຂົ້າໃຈແລ້ວ!" cancel: "ຍົກເລີກ" noThankYou: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້" enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້" -renotedBy: "Renoted ໂດຍ {user}" -noNotes: "ບໍ່ມີ note" +renotedBy: "Renoted ໂດຍ {ຜູ້ໃຊ້}" +noNotes: "ບໍ່ມີຫມາຍເຫດ" noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ" -instance: "ເຊີຟເວີຣ໌" -settings: "ຕັ້ງຄ່າ" +instance: "ອີນສະແຕນ" +settings: "ກຳນົດຄ່າ" notificationSettings: "ຕັ້ງຄ່າການແຈ້ງເຕືອນ" basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ" otherSettings: "ການຕັ້ງຄ່າອື່ນໆ" -openInWindow: "ເປີດໃນ window" -profile: "ໂປຣໄຟລ໌" -timeline: "ໄທມ໌ໄລນ໌" -noAccountDescription: "ຜູ້ໃຊ້ຄົນນີ້ຍັງບໍ່ໄດ້ຂຽນຄຳແນະນຳໂຕ" +openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ" +profile: "ໂພຼຟາຍ" +timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" +noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ" login: "ເຂົ້າ​ສູ່​ລະ​ບົບ" loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..." logout: "ອອກ​ຈາກ​ລະ​ບົບ" signup: "ລົງ​ທະ​ບຽນ" -uploading: "ກຳລັງອັບໂຫຼດ..." +uploading: "ການອັບໂຫຼດ..." save: "ບັນທຶກ" -users: "ຜູ້ໃຊ້" +users: "ຜູ້ໃຊ້ຕ່າງໆ" addUser: "ເພີ່ມຜູ້ໃຊ້" favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ" favorites: "ລາຍການທີ່ມັກ" -unfavorite: "ເອົາອອກຈາກລາຍການທີ່ມັກ" +unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ" favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ" alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ." cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້." -pin: "ປັກໝຸດ" -unpin: "ຖອດປັກໝຸດອອກ" +pin: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" +unpin: "ຖອດປັກໝຸດອອກຈາກໂປຣໄຟລ໌" copyContent: "ຄັດລອກເນື້ອຫາ" -copyLink: "ຄັດລອກລິ້ງ" -copyLinkRenote: "ຄັດລອກລິ້ງຂອງ renote" +copyLink: "ສຳເນົາລິ້ງ" delete: "ລຶບ" -deleteAndEdit: "ລຶບ​ແລະ​ແກ້​ໄຂ​" -deleteAndEditConfirm: "ຕ້ອງການລຶບ note ນີ້ແລະແກ້ໄຂໃໝ່ແມ່ນບໍ່? reaction, renote ແລະການຕອບກັບຕໍ່ note ນີ້ ທັງເບິດຈະຖືກລຶບອອກ" +deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​" +deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ" addToList: "ເພີ່ມໃສ່ລາຍຊື່" addToAntenna: "ເພີ່ມໃສ່ເສົາອາກາດ" sendMessage: "ສົ່ງຂໍ້ຄວາມ" -copyRSS: "ຄັດລອກ RSS" -copyUsername: "ຄັດລອກຊື່ຜູ້ໃຊ້" -copyUserId: "ຄັດລອກ ID ຜູ້ໃຊ້" -copyNoteId: "ຄັດລອກ ID ຂອງ note" -copyFileId: "ຄັດລອກ ID ໄຟລ໌" -copyFolderId: "ຄັດລອກ ID ໂຟລ໌ເດີຣ໌" -copyProfileUrl: "ຄັດລອກ URL ໂປຣໄຟລ໌" +copyRSS: "ສຳເນົາ RSS" +copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້" +copyUserId: "ສຳເນົາ ID ຜູ້ໃຊ້" +copyNoteId: "ສຳເນົາ ID ບັນທຶກ" +copyFileId: "ສຳເນົາ ID ໄຟລ໌" +copyFolderId: "ສຳເນົາ ID ໂຟນເດີ" +copyProfileUrl: "ສຳເນົາ URL ໂປຣໄຟລ໌" searchUser: "ຄົ້ນຫາຜູ້ໃຊ້" -reply: "ຕອບ​ກັບ" +reply: "ຕອບ​ໄປ​ທີ" loadMore: "ໂຫຼດເພີ່ມເຕີມ" showMore: "ໂຫຼດເພີ່ມເຕີມ" showLess: "ປິດ" -youGotNewFollower: "ໄດ້ຕິດຕາມເຈົ້າ" -receiveFollowRequest: "ມີຄຳຂໍຕິດຕາມສົ່ງມາ" -followRequestAccepted: "ການຕິດຕາມໄດ້ຮັບອນຸຍາດແລ້ວ" -mention: "ເວົ້າເຖີງ" -mentions: "ເວົ້າເຖີງເຈົ້າ" -directNotes: "ໂພສ Direct note" +youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ" +receiveFollowRequest: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ໄດ້ຮັບ" +followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້ຍອມຮັບຄໍາຮ້ອງຂໍຂອງທ່ານ" +mention: "ໄດ້ກ່າວມາ" +mentions: "ກ່າວເຖິງ" +directNotes: "ໂດຍກົງຫມາຍເຫດ" importAndExport: "ນໍາເຂົ້າ / ສົ່ງອອກ" import: "ນຳເຂົ້າ" -export: "ສົ່ງອອກ" +export: "ນຳອອກ" files: "ໄຟລ໌" download: "ດາວໂຫລດ" -driveFileDeleteConfirm: "ຕ້ອງການລຶບໄຟລ໌ “{name}” ແມ່ນບໍ່? Note ທີ່ແນບມາກັບໄຟລ໌ນີ້ຈະຖືກລຶບອອກ" -unfollowConfirm: "ຕ້ອງການເລີກຕິດຕາມ {name} ແມ່ນບໍ່?" -exportRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການສົ່ງອອກ ອາດໃຊ້ເວລາຈັກໜ່ອຍ ເມື່ອແລ້ວຈະຖືກເພີ່ມໃສ່ drive" -importRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການນຳເຂົ້າ ການດຳເນິນການນີ້ອາດໃຊ້ເວລາຈັກໜ່ອຍ" +driveFileDeleteConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບໄຟລ໌ \"{name}\"? ບັນທຶກທີ່ມີໄຟລ໌ແນບນີ້ຈະຖືກລຶບຖິ້ມ" +unfollowConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເຊົາຕິດຕາມ {name}?" +exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການສົ່ງອອກ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ ແລະມັນຈະຖືກເພີ່ມໃສ່ drive ຂອງທ່ານເມື່ອມັນສຳເລັດແລ້ວ" +importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການນໍາເຂົ້າ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ" lists: "ລາຍການ" -noLists: "ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​" -note: "Note" -notes: "Note" +noLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​" +note: "ບັນທຶກ" +notes: "ບັນທຶກ" following: "ກຳລັງຕິດຕາມ" followers: "ຜູ້ຕິດຕາມ" followsYou: "ຕິດ​ຕາມ​ເຈົ້າ" createList: "ສ້າງລາຍຊື່" -manageLists: "ຈັດການລາຍຊື່" +manageLists: "ການບໍລິຫານບັນຊີລາຍການ" error: "ຂໍ້ຜິດພາດ" somethingHappened: "​ອຸຍ, ມີ​ບາງ​ຢ່າງ​ຜິ​ດ​ພາດ" retry: "ລອງໃຫມ່" @@ -97,38 +96,38 @@ serverIsDead: "ເຊີບເວີນີ້ບໍ່ຕອບສະໜອງ youShouldUpgradeClient: "ເພື່ອເບິ່ງໜ້ານີ້, ກະລຸນາໂຫຼດຂໍ້ມູນຄືນໃໝ່ເພື່ອອັບເດດລູກຄ້າຂອງທ່ານ" enterListName: "ໃສ່ຊື່ສຳລັບລາຍຊື່" privacy: "ຄວາມເປັນສ່ວນຕົວ" -makeFollowManuallyApprove: "ຕິດຕາມຄຳຂໍທີ່ຕ້ອງໄດ້ຮັບການອະນຸມັດ" -defaultNoteVisibility: "ການເບິ່ງເຫັນທີ່ເປັນຄ່າເລີ່ມຕົ້ນ" +makeFollowManuallyApprove: "ປະຕິບັດຕາມການຮ້ອງຂໍຮຽກຮ້ອງໃຫ້ມີການອະນຸມັດ" +defaultNoteVisibility: "ເປັນຄ່າເລີ່ມຕົ້ນ" follow: "ກຳລັງຕິດຕາມ" -followRequest: "ສົ່ງ​ຄຳຂໍ​ຕິ​ດ​ຕາມ​" -followRequests: "ສົ່ງ​ຄຳຂໍ​ຕິ​ດ​ຕາມ​" +followRequest: "ສົ່ງ​ການ​ຮ້ອງ​ຂໍ​ປະ​ຕິ​ບ​ຕາມ​" +followRequests: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍ" unfollow: "ເຊົາຕິດຕາມ" -followRequestPending: "ລໍຖ້າການອະນຸມັດໃຫ້ຕິດຕາມ" -enterEmoji: "ປ້ອນເອໂມຈິ" +followRequestPending: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ລໍຖ້າຢູ່" +enterEmoji: "ປ້ອນອີໂມຈິ" renote: "Renote" unrenote: "ເລີກ Renote" -renoted: "renote ແລ້ວ" -cantRenote: "ໂພສນີ້ບໍ່ສາມາດ renote ໃໝ່ໄດ້" +renoted: "ເກັບບັນທຶກໄວ້" +cantRenote: "ໂພສນີ້ບໍ່ສາມາດຖືກບັນທຶກໄວ້ຄືນໃໝ່ໄດ້" cantReRenote: "ບໍ່ສາມາດບັນທຶກຄືນໃໝ່ໄດ້" -quote: "ອ້າງອີງ" -inChannelRenote: "Renote ໃນ channel ເທົ່ານັ້ນ" -inChannelQuote: "ອ້າງອິງໃນ channel ເທົ່ານັ້ນ" -pinnedNote: "note ທີ່ປັກໝຸດໄວ້" -pinned: "ປັກໝຸດ" +quote: "ລວມຂໍ້ຄວາມອ້າງອີງ" +inChannelRenote: "ຊ່ອງພຽງແຕ່ Renote" +inChannelQuote: "ຊ່ອງເທົ່ານັ້ນ Quote" +pinnedNote: "ບັນທຶກທີ່ປັກໝຸດໄວ້" +pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" you: "ເຈົ້າ" clickToShow: "ກົດເພື່ອສະແດງໃຫ້ເຫັນ" sensitive: "NSFW" add: "ເພີ່ມ" -reaction: "reaction" -reactions: "reaction" +reaction: "ປະຕິກິລິຍາ" +reactions: "ປະຕິກິລິຍາ" attachCancel: "ເອົາໄຟລ໌ແນບ" mute: "ປີດສຽງ" unmute: "ເປີດສຽງ" -block: "ບລັອກ" -unblock: "ເລີກບລັອກ" +block: "ບ໋ອກ" +unblock: "ຍົກເລີກກາຮົບລັອກ" suspend: "ລະງັບ" unsuspend: "ເຊົາ​ລະ​ງັບ" -selectList: "ເລືອກລາຍຊື່" +selectList: "ເລືອກບັນຊີລາຍການ" editList: "ແກ້ໄຂລາຍຊື່" selectChannel: "ເລືອກຊ່ອງ" selectAntenna: "ເລືອກເສົາອາກາດ" @@ -151,30 +150,30 @@ flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບ autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່" addAccount: "ເພີ່ມບັນຊີ" loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ" -showOnRemote: "ເບິ່ງໃນເຊີຟເວີຣ໌ໄລຍະໄກ" +showOnRemote: "ເບິ່ງຢູ່ໃນຕົວຢ່າງໄລຍະໄກ" general: "ທົ່ວໄປ" wallpaper: "ພາບພື້ນຫລັງ" setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ" removeWallpaper: "ລຶບຮູບວໍເປເປີອອກ" searchWith: "ຊອກຫາ: {q}" -youHaveNoLists: "ເຈົ້າບໍ່ມີລາຍຊື່ໃດໆ" +youHaveNoLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​" proxyAccount: "ບັນຊີພຣັອກຊີ" -host: "ໂຮສຕ໌" +host: "ໂຮດສ" selectUser: "ເລືອກຜູ້ໃຊ້" recipient: "ເຖິງ" annotation: "ຄຳເຫັນ" federation: "ສະຫະພັນ" -instances: "ເຊີຟເວີຣ໌" +instances: "ອີນສະແຕນ" registeredAt: "ລົງທະບຽນຢູ່" storageUsage: "ບ່ອນ​ຈັດ​ເກັບ​ຂໍ້​ມູນທີ່ໃຊ້" -charts: "ແຜນພູມ" +charts: "ອັນດັບເພງ" perHour: "ຕໍ່ຊົ່ວໂມງ" perDay: "ຕໍ່​ມື້" stopActivityDelivery: "ຢຸດເຊົາການສົ່ງກິດຈະກໍາ" blockThisInstance: "ຂັດຂວາງຕົວຢ່າງນີ້" operations: "ການດຳເນີນງານ" software: "ຊອບແວ" -version: "ເວີຣ໌ຊັນ" +version: "ສະບັບ" metadata: "Metadata" withNFiles: "{n} ໄຟລ໌(s)" monitor: "ຈໍພາບ" @@ -199,15 +198,15 @@ federating: "ສະຫະພັນ" blocked: "ບລັອກແລ້ວ " suspended: "ໂຈະ" all: "ທັງໝົດ" -subscribing: "ກຳລັງສະມັກສະມາຊິກ" -publishing: "ກຳລັງ​ເຜີຍ​ແພ່" +subscribing: "ສະໝັກສະມາຊິກແລັວ" +publishing: "ການ​ພິມ​ເຜີຍ​ແຜ່" notResponding: "ບໍ່ຕອບສະໜອງ" -instanceFollowing: "ກຳລັງຕິດຕາມບົນເຊີຟເວີຣ໌" -instanceFollowers: "ຜູ້ຕິດຕາມຂອງເຊີຟເວີຣ໌" -instanceUsers: "ຜູ້​ໃຊ້​ຂອງ​ເຊີຟເວີຣ໌ນີ້" +instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າງ" +instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ" +instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂອງ​ຕົວ​ຢ່າງ​ນີ້​" changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ" security: "ຄວາມປອດໄພ" -retypedNotMatch: "ປ້ອນຂໍ້ມູນບໍ່ກົງກັນ" +retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ" currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ" newPassword: "ລະຫັດຜ່ານໃໝ່" newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ" @@ -223,14 +222,14 @@ remove: "ລຶບ" removed: "ລຶບແລ້ວ" resetAreYouSure: "ຣີ​ເຊັດບໍ?" saved: "ບັນທຶກແລ້ວ" -messaging: "ແຊັຕ" +messaging: "ແຊ໋ດ" upload: "ອັບໂຫຼດ" keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ" fromDrive: "ຈາກ Drive" fromUrl: "ຈາກ URL" uploadFromUrl: "ອັບໂຫຼດຈາກ URL" uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ" -uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດແລ້ວ" +uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ" explore: "ສຳຫຼວດ" messageRead: "ອ່ານແລ້ວ" startMessaging: "ເລີ່ມການສົນທະນາໃໝ່" @@ -244,47 +243,47 @@ images: "ຮູບພາບ" image: "ຮູບພາບ" birthday: "ວັນເກີດ" yearsOld: "{age} ປີ" -registeredDate: "ວັນທີ່ລົງທະບຽນ" +registeredDate: "ວັນທີ່ເປັນສະມາຊິກ" location: "ທີ່ຕັ້ງ" -theme: "Theme" -themeForLightMode: "Theme ໃຊ້ໃນໂໝດສະຫວ່າງ" -themeForDarkMode: "Theme ໃຊ້ໃນໂໝດມືດ" +theme: "ແທ໋ມ" +themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ" +themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ" light: "ສະຫວ່າງ" dark: "ມືດ" lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ" darkThemes: "ຮູບແບບສີສັນມືດ" syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ" -drive: "Drive" +drive: "ຂັບ" fileName: "ຊື່ໄຟລ໌" selectFile: "ເລືອກໄຟລ໌" selectFiles: "ເລືອກໄຟລ໌" selectFolder: "ເລືອກໂຟລເດີ" selectFolders: "ເລືອກໂຟລເດີ" renameFile: "ປ່ຽນຊື່ໄຟລ໌" -folderName: "ຊື່ໂຟລເດີຣ໌" +folderName: "ຊື່ໂຟນເດີ" createFolder: "​ສ້າງ​ໂຟ​ລ​ເດີ" renameFolder: "ປ່ຽນຊື່ໂຟນເດີນີ້" deleteFolder: "ລົບໂຟ​ລ​ເດີ​" addFile: "ເພີ່ມໄຟລ໌" emptyDrive: "Drive ຂອງທ່ານຫວ່າງເປົ່າ" -emptyFolder: "ໂຟລເດີຣ໌ນີ້ວ່າງເປົ່າ" +emptyFolder: "ໂຟນເດີນີ້ເປົ່າຫວ່າງ" unableToDelete: "ບໍ່​ສາ​ມາດລົບໄດ້" inputNewFileName: "ໃສ່ຊື່ໄຟລ໌ໃໝ່" inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່" inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່" circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ" rename: "ປ່ຽນຊື່" -doNothing: "ຢ່າມັນ" -watch: "ເພັ່ງເລັງ" -unwatch: "ຢຸດເພັ່ງເລັງ" +doNothing: "ບໍ່ສົນໃຈ" +watch: "ເບິ່ງ" +unwatch: "ຢຸດເບິ່ງ" accept: "ອະນຸຍາດ" reject: "ປະຕິເສດ" normal: "ປົກກະຕິ" instanceName: "ຊື່ເຊີເວີ້" -instanceDescription: "ຄຳອະທິບາຍແນະນຳເຊີຟເວີຣ໌" +instanceDescription: "ຄໍາອະທິບາຍຕົວຢ່າງ" maintainerName: "ຜູ້ດູແລ" -maintainerEmail: "ອີເມລຜູ້ດູແລ" -tosUrl: " URL ເງື່ອນໄຂການໃຫ້ບໍລິການ" +maintainerEmail: "ອີເມວ admin" +tosUrl: "ເງື່ອນໄຂການໃຫ້ບໍລິການ URL" thisYear: "ປີນີ້" thisMonth: "ເດືອນນີ້" today: "ມື້ນີ້" @@ -292,34 +291,32 @@ dayX: "ວັນ {day}" monthX: "ເດືອນ {month}" yearX: "ປີ {year}" pages: "ໜ້າ" -integration: "ເຊື່ອມໂຍງ" +integration: "ຄວາມສຳພັນຂອງ" connectService: "ເຊື່ອມຕໍ່" disconnectService: "ຕັດການເຊື່ອມຕໍ່" enableLocalTimeline: "ເປີດໃຊ້ທາມລາຍທ້ອງຖິ່ນ" enableGlobalTimeline: "ເປີດໃຊ້ທາມລາຍທົ່ວໂລກ" -disablingTimelinesInfo: "ຜູ້ດູແລລະບບແລະຜູ້ຄວບຄຸມຈະສາມາດເຂົ້າເຖີງໄທມ໌ໄລນ໌ທັ້ງເບີດ ເຖີງວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍ່ຕາມ" +disablingTimelinesInfo: "ຜູ້ເບິ່ງແຍງລະບົບ ແລະຜູ້ຄວບຄຸມຈະມີການເຂົ້າເຖິງທຸກກຳນົດເວລາ, ເຖິງແມ່ນວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍຕາມ" registration: "ລົງທະບຽນ" enableRegistration: "ເປີດໃຊ້ການລົງທະບຽນຜູ້ໃຊ້ໃໝ່" invite: "ເຊີນ" -driveCapacityPerLocalAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ" -driveCapacityPerRemoteAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ໄລຍະໄກ" +driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ" +driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ" basicInfo: "ຂໍ້ມຸນເບື້ອງຕົ້ນ" -pinnedNotes: "Note ທີ່ປັກໝຸດໄວ້" -hcaptchaSiteKey: "Site key" -hcaptchaSecretKey: "Secret key" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret Key" +pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້" +hcaptchaSiteKey: "ກະແຈໄຊທ໌" +hcaptchaSecretKey: "ກະແຈລັບ" recaptcha: "reCAPTCHA" -enableRecaptcha: "ເປີດໃຊ້ງານ reCAPTCHA" -recaptchaSiteKey: "Site key" -recaptchaSecretKey: "Secret key" -turnstileSiteKey: "Site key" -turnstileSecretKey: "Secret key" +enableRecaptcha: "ເປີດໃຊ້ງານລີແຄ໋ບຈາ" +recaptchaSiteKey: "ກະແຈໄຊທ໌" +recaptchaSecretKey: "ກະແຈລັບ" +turnstileSiteKey: "ກະແຈໄຊທ໌" +turnstileSecretKey: "ກະແຈລັບ" name: "ຊື່" userList: "ລາຍການ" about: "ກ່ຽວກັບ" aboutMisskey: "ກ່ຽວກັບ CherryPick" -administrator: "ຜູ້ດູແລ" +administrator: "ຜູ້ບໍລິຫານ" token: "ໂທເຄັນ" share: "ແບ່ງປັນ" notFound: "ບໍ່ພົບ" @@ -332,27 +329,27 @@ title: "ຫົວຂໍ້" text: "ຂໍ້ຄວາມ" enable: "ເປີດໃຊ້" next: "ຕໍ່ໄປ" -retype: "ລອງພິມລະຫັດອີກເທື່ອໜຶ່ງ" -quoteAttached: "ອ້າງອິງ" +retype: "ເຂົ້າໄປອີກຄັ້ງ" +quoteAttached: "ວົງຢືມ" invitations: "ເຊີນ" unavailable: "ບໍ່​ສາ​ມາດ​ໃຊ້​ໄດ້" language: "ພາສາ" aboutX: "ກ່ຽວກັບ {x}" emojiStyle: "ຮູບແບບອີໂມຈິ" native: "ພາ​ສາ​ແມ່" -noHistory: "​ບໍ່​ມີປະຫວັດ" +noHistory: "​ບໍ່​ມີ​ລາຍ​ການ​ຢູ່​ບ່ອນ​ນີ້" doing: "ກຳລັງປະມວນຜົນ..." category: "ຫມວດຫມູ່" -tags: "Aliases" +tags: "ແທ໋ກ" createAccount: "ສ້າງບັນຊີ" -existingAccount: "ບັນຊີທີ່ມີຢູ່ແລ້ວ" -dashboard: "Dashboard" +existingAccount: "ທີ່ມີຢູ່" +dashboard: "ໜ້າປັດ" local: "ທ້ອງຖິ່ນ" numberOfDays: "ຈຳນວນມື້" objectStorageBucket: "Bucket" objectStoragePrefix: "Prefix" objectStorageEndpoint: "Endpoint" -objectStorageRegion: "ພູມິພາກ" +objectStorageRegion: "ພາກ​ພື້ນ" deleteAll: "ລຶບທັງໝົດ" sounds: "ສຽງ" sound: "ສຽງ" @@ -365,11 +362,11 @@ state: "ສະຖານະ" sort: "ຈັດຮຽງໂດຍ" ascendingOrder: "ນ້ອຍໄປຫາໃຫຍ່" descendingOrder: "ໃຫຍ່ຫານ້ອຍ" -output: "Output" -script: "Script" +output: "ຜົນຜະລິດ" +script: "ບົດ​ຄວາມ" menu: "ເມນູ" -rearrange: "ຈັດລຽງໃໝ່" -poll: "Poll" +rearrange: "ຈັດລຽງຄືນ" +poll: "ການພູນ" description: "ລາຍລະອຽດ" author: "ຜູ້ຂຽນ" manage: "ການຈັດການ" @@ -383,7 +380,7 @@ permission: "ການອະນຸຍາດ" notificationType: "​ປະເພດການ​ແຈ້ງ​ເຕືອນ" edit: "ແກ້ໄຂ" email: "ອີເມວ" -smtpHost: "ໂຮສຕ໌" +smtpHost: "ໂຮດສ" smtpUser: "ຊື່ຜູ້ໃຊ້" smtpPass: "ລະຫັດຜ່ານ" clearCache: "ລຶບລ້າງແຄສ" @@ -393,12 +390,8 @@ administration: "ການຈັດການ" middle: "ປານກາງ" searchByGoogle: "ຄົ້ນຫາ" file: "ໄຟລ໌" -replies: "ຕອບ​ກັບ" +replies: "ຕອບ​ໄປ​ທີ" renotes: "Renote" -_delivery: - stop: "ໂຈະ" - _type: - none: "ກຳລັງ​ເຜີຍ​ແພ່" _role: _priority: middle: "ປານກາງ" @@ -417,8 +410,8 @@ _sfx: _2fa: renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້" _widgets: - profile: "ໂປຣໄຟລ໌" - instanceInfo: "ຂໍ້ມູລເຊີຟເວີຣ໌" + profile: "ໂພຼຟາຍ" + instanceInfo: "ອີນສະແຕນ" notifications: "ການແຈ້ງເຕືອນ" timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" activity: "ກິດຈະກຳ" @@ -437,28 +430,28 @@ _profile: _exportOrImport: followingList: "ກຳລັງຕິດຕາມ" muteList: "ປີດສຽງ" - blockingList: "ບລັອກ" + blockingList: "ບ໋ອກ" userLists: "ລາຍການ" _charts: federation: "ສະຫະພັນ" _timelines: home: "ໜ້າຫຼັກ" _play: - script: "Script" + script: "ບົດ​ຄວາມ" summary: "ລາຍລະອຽດ" _pages: blocks: image: "ຮູບພາບ" _notification: - youWereFollowed: "ໄດ້ຕິດຕາມເຈົ້າ" + youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ" _types: follow: "ກຳລັງຕິດຕາມ" - mention: "ໄດ້ກ່າວເຖິງ" + mention: "ໄດ້ກ່າວມາ" renote: "Renote" - quote: "ອ້າງອີງ" - reaction: "Reaction" + quote: "ລວມຂໍ້ຄວາມອ້າງອີງ" + reaction: "ປະຕິກິລິຍາ" _actions: - reply: "ຕອບ​ກັບ" + reply: "ຕອບ​ໄປ​ທີ" renote: "Renote" _deck: _columns: @@ -466,12 +459,8 @@ _deck: tl: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" list: "ລາຍການ" channel: "ຊ່ອງ" - mentions: "ກ່າວເຖິງເຈົ້າ" + mentions: "ກ່າວເຖິງ" _webhookSettings: name: "ຊື່" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "ອີເມວ" _moderationLogTypes: suspend: "ລະງັບ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 1ccdb970e6..a29e3738a8 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -348,8 +348,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Inschakelen hCaptcha" hcaptchaSiteKey: "Site sleutel" hcaptchaSecretKey: "Geheime sleutel" -mcaptchaSiteKey: "Site sleutel" -mcaptchaSecretKey: "Geheime sleutel" recaptcha: "reCAPTCHA" enableRecaptcha: "Inschakelen reCAPTCHA" recaptchaSiteKey: "Site sleutel" @@ -429,10 +427,6 @@ loggedInAsBot: "Momenteel als bot ingelogd" icon: "Avatar" replies: "Antwoord" renotes: "Herdelen" -_delivery: - stop: "Opgeschort" - _type: - none: "Publiceren" _email: _follow: title: "volgde jou" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 0893523a9f..c21bf65ced 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -463,9 +463,6 @@ options: "Alternativ" icon: "Avatar" replies: "Svar" renotes: "Renote" -surrender: "Avbryt" -_delivery: - stop: "Suspendert" _initialAccountSetting: theseSettingsCanEditLater: "Du kan endre disse innstillingene senere." _achievements: @@ -723,9 +720,5 @@ _deck: direct: "Direkte" _webhookSettings: name: "Navn" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-post" _moderationLogTypes: suspend: "Suspender" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 818046ddcd..94afb06a9f 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -20,7 +20,6 @@ noNotes: "Brak wpisów" noNotifications: "Brak powiadomień" instance: "Instancja" settings: "Ustawienia" -notificationSettings: "Powiadomienia" basicSettings: "Podstawowe ustawienia" otherSettings: "Pozostałe ustawienia" openInWindow: "Otwórz w oknie" @@ -45,20 +44,13 @@ pin: "Przypnij do profilu" unpin: "Odepnij z profilu" copyContent: "Skopiuj zawartość" copyLink: "Skopiuj odnośnik" -copyLinkRenote: "Skopiuj link renote'a" delete: "Usuń" deleteAndEdit: "Usuń i edytuj" deleteAndEditConfirm: "Czy na pewno chcesz usunąć ten wpis i zedytować go? Utracisz wszystkie reakcje, udostępnienia i odpowiedzi do tego wpisu." addToList: "Dodaj do listy" -addToAntenna: "Dodaj do anteny" sendMessage: "Wyślij wiadomość" copyRSS: "Kopiuj RSS" copyUsername: "Kopiuj nazwę użytkownika" -copyUserId: "Kopiuj ID użytkownika" -copyNoteId: "Kopiuj ID notatki" -copyFileId: "Kopiuj ID pliku" -copyFolderId: "Kopiuj ID folderu" -copyProfileUrl: "Kopiuj URL profilu" searchUser: "Wyszukiwanie użytkowników" reply: "Odpowiedz" loadMore: "Załaduj więcej" @@ -111,8 +103,6 @@ renoted: "Udostępniono." cantRenote: "Ten wpis nie może zostać udostępniony." cantReRenote: "Udostępnienie nie może zostać udostępnione." quote: "Cytuj" -inChannelRenote: "Renote tylko na kanale" -inChannelQuote: "Cytat tylko na kanale" pinnedNote: "Przypięty wpis" pinned: "Przypnij do profilu" you: "Ty" @@ -121,23 +111,14 @@ sensitive: "NSFW" add: "Dodaj" reaction: "Reakcja" reactions: "Reakcja" -emojiPicker: "Selektor Emoji" -pinnedEmojisForReactionSettingDescription: "Ustaw emotikony które powinny być przypięte i od razu wyświetlone podczas reagowania." -pinnedEmojisSettingDescription: "Ustaw emotikony które powinny być przypięte i wyświetlone podczas przeglądania selektora Emoji" -emojiPickerDisplay: "Wyświetlanie selektora Emoji" -overwriteFromPinnedEmojisForReaction: "Zastąp z ustawień reakcji" -overwriteFromPinnedEmojis: "Zastąp z ogólnych ustawień" reactionSettingDescription2: "Przeciągnij aby zmienić kolejność, naciśnij aby usunąć, naciśnij „+” aby dodać" rememberNoteVisibility: "Zapamiętuj ustawienia widoczności wpisu" attachCancel: "Usuń załącznik" -deleteFile: "Usuń plik" markAsSensitive: "Oznacz jako NSFW" unmarkAsSensitive: "Cofnij NSFW" enterFileName: "Wprowadź nazwę pliku" mute: "Wycisz" unmute: "Cofnij wyciszenie" -renoteMute: "Wycisz renote'y" -renoteUnmute: "Wyłącz wyciszenie renote'ów" block: "Zablokuj" unblock: "Odblokuj" suspend: "Zawieś" @@ -147,10 +128,8 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?" suspendConfirm: "Czy na pewno chcesz zawiesić to konto?" unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?" selectList: "Wybierz listę" -editList: "Edytuj listę" selectChannel: "Wybierz kanał" selectAntenna: "Wybierz Antennę" -editAntenna: "Edytuj antenę" selectWidget: "Wybierz widżet" editWidgets: "Edytuj widżety" editWidgetsExit: "Gotowe" @@ -163,15 +142,11 @@ addEmoji: "Dodaj emoji" settingGuide: "Proponowana konfiguracja" cacheRemoteFiles: "Przechowuj zdalne pliki w pamięci podręcznej" cacheRemoteFilesDescription: "Gdy ta opcja jest wyłączona, zdalne pliki są ładowane bezpośrednio ze zdalnych instancji. Wyłączenie the opcji zmniejszy użycie powierzchni dyskowej, ale zwiększy transfer, ponieważ miniaturki nie będą generowane." -youCanCleanRemoteFilesCache: "Możesz wyczyścić cache poprzez kliknięcie przycisku 🗑️ w widoku menedżera plików." -cacheRemoteSensitiveFiles: "Przechowuj wrażliwe zdalne pliki w pamięci podręcznej" -cacheRemoteSensitiveFilesDescription: "Gdy ta opcja jest wyłączona, wrażliwe pliki zdalne są wczytywane bezpośrednio ze zdalnej instancji bez cacheowania." flagAsBot: "To konto jest botem" flagAsBotDescription: "Jeżeli ten kanał jest kontrolowany przez jakiś program, ustaw tę opcję. Jeżeli włączona, będzie działać jako flaga informująca innych programistów, aby zapobiegać nieskończonej interakcji z różnymi botami i dostosowywać wewnętrzne systemy CherryPick, traktując konto jako bota." flagAsCat: "To konto jest kotem" flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot." flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu" -flagShowTimelineRepliesDescription: "Gdy włączone, pokazuje odpowiedzi użytkowników na notatki innych użytkowników w osi czasu." autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz" addAccount: "Dodaj konto" reloadAccountsList: "Odśwież listę kont" @@ -201,7 +176,6 @@ perHour: "co godzinę" perDay: "co dzień" stopActivityDelivery: "Przestań przesyłać aktywności" blockThisInstance: "Zablokuj tę instancję" -silenceThisInstance: "Wycisz tę instancję" operations: "Działania" software: "Oprogramowanie" version: "Wersja" @@ -221,8 +195,6 @@ clearCachedFiles: "Wyczyść pamięć podręczną" clearCachedFilesConfirm: "Czy na pewno chcesz usunąć wszystkie zdalne pliki z pamięci podręcznej?" blockedInstances: "Zablokowane instancje" blockedInstancesDescription: "Wypisz nazwy hostów instancji, które powinny zostać zablokowane. Wypisane instancje nie będą mogły dłużej komunikować się z tą instancją." -silencedInstances: "Wyciszone instancje" -silencedInstancesDescription: "Wypisz nazwy hostów instancji, które chcesz wyciszyć. Wszystkie konta wymienionych instancji będą traktowane jako wyciszone, będą mogły jedynie wysyłać prośby o obserwację i nie będą mogły wspominać kont lokalnych, jeśli nie będą obserwowane. Nie będzie to miało wpływu na zablokowane instancje." muteAndBlock: "Wycisz / Zablokuj" mutedUsers: "Wyciszeni użytkownicy" blockedUsers: "Zablokowani użytkownicy" @@ -267,12 +239,10 @@ removed: "Pomyślnie usunięto" removeAreYouSure: "Czy na pewno chcesz usunąć „{x}”?" deleteAreYouSure: "Czy na pewno chcesz usunąć „{x}”?" resetAreYouSure: "Czy na pewno chcesz zresetować?" -areYouSure: "Na pewno?" saved: "Zapisano" messaging: "Wiadomości" upload: "Wyślij" keepOriginalUploading: "Zachowaj oryginalny obraz" -keepOriginalUploadingDescription: "Zapisuje oryginalnie przesłany obraz w niezmienionej postaci. Jeśli ta opcja jest wyłączona, po przesłaniu zostanie wygenerowana wersja do wyświetlenia w Internecie." fromDrive: "Z dysku" fromUrl: "Z adresu URL" uploadFromUrl: "Wyślij z adresu URL" @@ -285,10 +255,7 @@ noMoreHistory: "Nie ma dalszej historii" startMessaging: "Rozpocznij czat" nUsersRead: "przeczytano przez {n}" agreeTo: "Wyrażam zgodę na {0}" -agree: "Zatwierdź" agreeBelow: "Zaakceptuj poniżej" -basicNotesBeforeCreateAccount: "Ważne notatki" -termsOfService: "Warunki usługi" start: "Rozpocznij" home: "Strona główna" remoteUserCaution: "Te informacje mogą nie być aktualne, ponieważ użytkownik pochodzi ze zdalnej instancji." @@ -318,7 +285,6 @@ folderName: "Nazwa katalogu" createFolder: "Utwórz katalog" renameFolder: "Zmień nazwę katalogu" deleteFolder: "Usuń ten katalog" -folder: "Folder" addFile: "Dodaj plik" emptyDrive: "Dysk jest pusty" emptyFolder: "Ten katalog jest pusty" @@ -332,7 +298,6 @@ copyUrl: "Skopiuj adres URL" rename: "Zmień nazwę" avatar: "Awatar" banner: "Baner" -displayOfSensitiveMedia: "Wyświetlanie wrażliwej zawartości" whenServerDisconnected: "Po utracie połączenia z serwerem" disconnectedFromServer: "Utracono połączenie z serwerem." reload: "Odśwież" @@ -380,11 +345,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Włącz hCaptcha" hcaptchaSiteKey: "Klucz strony" hcaptchaSecretKey: "Tajny klucz" -mcaptcha: "mCaptcha" -enableMcaptcha: "Włącz mCaptcha" -mcaptchaSiteKey: "Klucz strony" -mcaptchaSecretKey: "Tajny klucz" -mcaptchaInstanceUrl: "URL instancji mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Włącz reCAPTCHA" recaptchaSiteKey: "Klucz strony" @@ -427,19 +387,15 @@ aboutMisskey: "O CherryPick" administrator: "Admin" token: "Token" 2fa: "Klucz 2FA " -setupOf2fa: "Skonfiguruj dwuetapową autentykację" totp: "Klucz aplikacji uwierzytelniającej (totp)" totpDescription: "Opis klucza czasowego" moderator: "Moderator" moderation: "Moderacja" -moderationNote: "Notka moderacyjna" -addModerationNote: "Dodaj notkę moderacyjną" -moderationLogs: "Logi moderacyjne" nUsersMentioned: "{n} wspomnianych użytkowników" securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey" securityKey: "Klucz bezpieczeństwa" lastUsed: "Ostatnio używane" -lastUsedAt: "Ostatnio używane: {t}" +lastUsedAt: "Ostatnio używane w" unregister: "Cofnij rejestrację" passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła" passwordLessLoginDescription: "Opis logowania bez użycia hasła" @@ -505,12 +461,8 @@ native: "Natywny" disableDrawer: "Nie używaj menu w stylu szuflady" youHaveNoGroups: "Nie masz żadnych grup" joinOrCreateGroup: "Uzyskaj zaproszenie do dołączenia do grupy lub utwórz własną grupę." -showNoteActionsOnlyHover: "Pokazuj akcje notatek tylko po najechaniu myszką" -showReactionsCount: "Wyświetl liczbę reakcji na notatkę" noHistory: "Brak historii" signinHistory: "Historia logowania" -enableAdvancedMfm: "Włącz zaawansowane MFM" -enableAnimatedMfm: "Włącz animowane MFM" doing: "Przetwarzanie..." category: "Kategoria" tags: "Tagi" @@ -519,8 +471,6 @@ createAccount: "Utwórz konto" existingAccount: "Istniejące konto" regenerate: "Wygeneruj ponownie" fontSize: "Rozmiar czcionki" -mediaListWithOneImageAppearance: "Wysokość list multimediów z tylko jednym obrazem" -limitTo: "Limituj do {x}" noFollowRequests: "Nie masz żadnych oczekujących próśb o możliwość obserwacji" openImageInNewTab: "Otwórz obraz w nowej karcie" dashboard: "Kokpit" @@ -540,7 +490,6 @@ showFeaturedNotesInTimeline: "Pokazuj wyróżnione wpisy w osi czasu" objectStorage: "Pamięć obiektowa" useObjectStorage: "Używaj pamięci obiektowej" objectStorageBaseUrl: "Podstawowy URL" -objectStorageBaseUrlDesc: "Adres URL używany jako odniesienie. Podaj adres URL swojego CDN lub Proxy, gdy używasz któregokolwiek z nich.\nDla S3 użyj 'https://.s3.amazonaws.com' a dla GCS lub równej usługi użyj 'https://storage.googleapis.com/', itd." objectStorageBucket: "Bucket" objectStorageBucketDesc: "Podaj nazwę „wiadra” używaną przez konfigurowaną usługę." objectStoragePrefix: "Prefiks" @@ -553,13 +502,9 @@ objectStorageUseSSL: "Użyj SSL" objectStorageUseSSLDesc: "Wyłącz, jeżeli nie zamierzasz używać HTTPS dla połączenia z API" objectStorageUseProxy: "Połącz przez proxy" objectStorageUseProxyDesc: "Wyłącz, jeżeli nie zamierzasz używać proxy dla połączenia z pamięcią blokową" -objectStorageSetPublicRead: "Ustaw opcję \"public-read\" przy przesyłaniu" -s3ForcePathStyleDesc: "Jeśli opcja s3ForcePathStyle jest włączona, nazwa Bucket'u musi być zawarta w ścieżce adresu URL, a nie w nazwie hosta adresu URL. Włączenie tego ustawienia może być konieczne w przypadku użycia usług takich jak self-hosted instancja Minio." serverLogs: "Dziennik zdarzeń" deleteAll: "Usuń wszystkie" showFixedPostForm: "Wyświetlaj formularz tworzenia wpisu w górnej części osi czasu" -showFixedPostFormInChannel: "Wyświetl formularz postowania w górnej części osi czasu (Kanały)" -withRepliesByDefaultForNewlyFollowed: "Domyślnie uwzględnij odpowiedzi nowo obserwowanych użytkowników w osi czasu" newNoteRecived: "Masz nowy wpis" sounds: "Dźwięk" sound: "Dźwięki" @@ -569,8 +514,6 @@ showInPage: "Pokaż na stronie" popout: "Popout" volume: "Głośność" masterVolume: "Głośność główna" -notUseSound: "Wyłącz dźwięk" -useSoundOnlyWhenActive: "Puszczaj dźwięki tylko, gdy CherryPick jest aktywne." details: "Szczegóły" chooseEmoji: "Wybierz emoji" unableToProcess: "Nie udało się dokończyć działania." @@ -591,10 +534,6 @@ output: "Wyjście" script: "Skrypt" disablePagesScript: "Wyłącz AiScript na Stronach" updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku" -unsetUserAvatar: "Usuń awatar" -unsetUserAvatarConfirm: "Czy na pewno chcesz usunąć awatar tego użytkownika?" -unsetUserBanner: "Usuń baner" -unsetUserBannerConfirm: "Czy na pewno chcesz usunąć baner?" deleteAllFiles: "Usuń wszystkie pliki" deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?" removeAllFollowing: "Przestań obserwować" @@ -610,7 +549,6 @@ accountDeletedDescription: "Opis konta usuniętego" menu: "Menu" divider: "Rozdzielacz" addItem: "Dodaj element" -rearrange: "Posortuj" relays: "Przekaźniki" addRelay: "Dodaj przekaźnik" inboxUrl: "Adres URL skrzynki nadawczej" @@ -645,7 +583,6 @@ medium: "Średnie" small: "Małe" generateAccessToken: "Generuj token dostępu" permission: "Uprawnienia" -adminPermission: "Uprawnienia administracyjne" enableAll: "Włącz wszystko" disableAll: "Wyłącz wszystko" tokenRequested: "Przydziel dostęp do konta" @@ -663,12 +600,9 @@ smtpPort: "Port" smtpUser: "Nazwa użytkownika" smtpPass: "Hasło" emptyToDisableSmtpAuth: "Pozostaw adres e-mail i hasło puste, aby wyłączyć weryfikację SMTP" -smtpSecure: "Użyj niejawnego SSL/TLS dla połączeń SMTP" smtpSecureInfo: "Wyłącz, jeżeli używasz STARTTLS" testEmail: "Przetestuj dostarczanie wiadomości e-mail" wordMute: "Wyciszenie słowa" -regexpError: "Błąd wyrażenia regularnego" -regexpErrorDescription: "Wystąpił błąd w wyrażeniu regularnym w linii {line} twoich {tab} wyciszeń:" instanceMute: "Wyciszone instancje" userSaysSomething: "{name} powiedział(-a) coś" makeActive: "Aktywuj" @@ -688,22 +622,18 @@ useGlobalSettingDesc: "Jeżeli włączone, zostaną wykorzystane ustawienia powi other: "Inne" regenerateLoginToken: "Generuj token logowania ponownie" regenerateLoginTokenDescription: "Regeneruje token używany wewnętrznie podczas logowania. Zazwyczaj nie jest to konieczne. Po regeneracji wszystkie urządzenia zostaną wylogowane." -theKeywordWhenSearchingForCustomEmoji: "To jest słowo kluczowe używane podczas wyszukiwania customowych Emoji." setMultipleBySeparatingWithSpace: "Możesz ustawić wiele, oddzielając je spacjami." fileIdOrUrl: "ID pliku albo URL" behavior: "Zachowanie" sample: "Przykład" abuseReports: "Zgłoszenia" reportAbuse: "Zgłoś" -reportAbuseRenote: "Zgłoś renote" reportAbuseOf: "Zgłoś {name}" fillAbuseReportDescription: "Wypełnij szczegóły zgłoszenia. Jeżeli dotyczy ono określonego wpisu, uwzględnij jego adres URL." abuseReported: "Twoje zgłoszenie zostało wysłane. Dziękujemy." -reporter: "Zgłaszający" reporteeOrigin: "Pochodzenie zgłoszonego" reporterOrigin: "Pochodzenie zgłaszającego" forwardReport: "Przekaż zgłoszenie do innej instancji" -forwardReportIsAnonymous: "Zamiast twojego konta, anonimowe konto systemowe będzie wyświetlone jako zgłaszający na instancji zdalnej." send: "Wyślij" abuseMarkAsResolved: "Oznacz zgłoszenie jako rozwiązane" openInNewTab: "Otwórz w nowej karcie" @@ -748,7 +678,6 @@ lockedAccountInfo: "Dopóki nie ustawisz widoczności wpisu na \"Obserwujący\", alwaysMarkSensitive: "Oznacz domyślnie jako NSFW" loadRawImages: "Wyświetlaj zdjęcia w załącznikach w całości zamiast miniatur" disableShowingAnimatedImages: "Nie odtwarzaj animowanych obrazów" -highlightSensitiveMedia: "Podkreśl wrażliwą zawartość" verificationEmailSent: "Wiadomość weryfikacyjna została wysłana. Odwiedź uwzględniony odnośnik, aby ukończyć weryfikację." notSet: "Nie ustawiono" emailVerified: "Adres e-mail został potwierdzony" @@ -759,8 +688,6 @@ contact: "Kontakt" useSystemFont: "Używaj domyślnej czcionki systemu" clips: "Klipy" experimentalFeatures: "Eksperymentalne funkcje" -experimental: "Eksperymentalne" -thisIsExperimentalFeature: "Ta funkcja jest eksperymentalna. Jej funkcjonalność może ulec zmianie, i może ona nie funkcjonować tak, jak zamierzono." developer: "Programista" makeExplorable: "Pokazuj konto na stronie „Eksploruj”" makeExplorableDescription: "Jeżeli wyłączysz tę opcję, Twoje konto nie będzie wyświetlać się w sekcji „Eksploruj”." @@ -778,14 +705,12 @@ onlineUsersCount: "{n} osób jest online" nUsers: "{n} użytkowników" nNotes: "{n} wpisów" sendErrorReports: "Wyślij raporty o błędach" -sendErrorReportsDescription: "Gdy włączone, jeśli wystąpi problem, szczegółowe informacje o błędach będą udostępniane CherryPick, pomagając ulepszyć jakość CherryPick.\nBędzie to zawierało informacje takie jak wersja twojego systemu operacyjnego, jakiej przeglądarki używasz, twoja aktywność w CherryPick, itd." myTheme: "Mój motyw" backgroundColor: "Tło" accentColor: "Akcent" textColor: "Tekst" saveAs: "Zapisz jako…" advanced: "Zaawansowane" -advancedSettings: "Zaawansowane ustawienia" value: "Wartość" createdAt: "Utworzono" updatedAt: "Zaktualizowano" @@ -845,14 +770,12 @@ noMaintainerInformationWarning: "Informacje o administratorze nie są skonfiguro noBotProtectionWarning: "Zabezpieczenie przed botami nie jest skonfigurowane." configure: "Skonfiguruj" postToGallery: "Opublikuj w galerii" -postToHashtag: "Postuj do tego hashtagu" gallery: "Galeria" recentPosts: "Ostatnie wpisy" popularPosts: "Popularne wpisy" shareWithNote: "Udostępnij z wpisem" ads: "Reklamy" expiration: "Ankieta kończy się" -startingperiod: "Początek" memo: "Notatki" priority: "Priorytet" high: "Wysoki" @@ -879,19 +802,13 @@ translatedFrom: "Przetłumaczone z {x}" accountDeletionInProgress: "Trwa usuwanie konta" usernameInfo: "Nazwa, która identyfikuje Twoje konto spośród innych na tym serwerze. Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreślników (_). Nazwy użytkownika nie mogą być później zmieniane." aiChanMode: "Tryb Ai" -devMode: "Tryb programisty" keepCw: "Zostaw ostrzeżenia o zawartości" pubSub: "Konta Pub/Sub" -lastCommunication: "Ostatnia komunikacja" resolved: "Rozwiązane" unresolved: "Nierozwiązane" breakFollow: "Usuń obserwującego" -breakFollowConfirm: "Czy na pewno usunąć tego obserwującego?" itsOn: "Włączone" itsOff: "Wyłączone" -on: "Włączone" -off: "Wyłączone" -emailRequiredForSignup: "Wymagaj adresu e-mail do rejestracji" unread: "Nieodczytane" filter: "Filtr" controlPanel: "Panel sterowania" @@ -901,8 +818,6 @@ makeReactionsPublicDescription: "To spowoduje, że lista wszystkich Twoich dotyc classic: "Klasyczny" muteThread: "Wycisz wątek" unmuteThread: "Wyłącz wyciszenie wątku" -followingVisibility: "Widoczność obserwacji" -followersVisibility: "Widoczność obserwujących" continueThread: "Pokaż kontynuację wątku" deleteAccountConfirm: "Spowoduje to nieodwracalne usunięcie Twojego konta. Kontynuować?" incorrectPassword: "Nieprawidłowe hasło." @@ -917,14 +832,9 @@ overridedDeviceKind: "Typ urządzenia" smartphone: "Smartfon" tablet: "Tablet" auto: "Automatycznie" -themeColor: "Motyw kolorystyczny" size: "Rozmiar" numberOfColumn: "Liczba kolumn" searchByGoogle: "Szukaj" -instanceDefaultLightTheme: "Domyślny motyw dla trybu jasnego" -instanceDefaultDarkTheme: "Domyślny motyw dla trybu ciemnego" -instanceDefaultThemeDescription: "Opis domyślnego motywu instancji" -mutePeriod: "Okres wyciszenia" period: "Ankieta kończy się" indefinitely: "Nigdy" tenMinutes: "10 minut" @@ -933,50 +843,29 @@ oneDay: "1 dzień" oneWeek: "1 tydzień" oneMonth: "jeden miesiąc" failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie" -rateLimitExceeded: "Limit szybkości przekroczony" -cropImage: "Przytnij obraz" -cropImageAsk: "Czy chcesz przyciąć obrazek?" -cropYes: "Tak, przytnij" -cropNo: "Nie chce przycinać" file: "Pliki" -recentNHours: "W ciągu ostatnich {n} godzin" -recentNDays: "W ciągu ostatnich {n} dni" -noEmailServerWarning: "Serwer Email nie jest skonfigurowany" recommended: "Zalecane" check: "Zweryfikuj" -driveCapOverrideLabel: "Zmień limit pojemności dysku użytkownika" -requireAdminForView: "Aby to zobaczyć, musisz być administratorem" -isSystemAccount: "To jest konto stworzone i zarządzane przez system" -typeToConfirm: "Wprowadź {x}, aby potwierdzić" deleteAccount: "Usuń konto" document: "Dokumentacja" numberOfPageCache: "Ilość stron w cache" -numberOfPageCacheDescription: "Zwiększenie tej liczby polepszy wygodę, ale spowoduje większe obciążenie jako użycie pamięci na urządzeniu użytkownika." logoutConfirm: "Czy na pewno chcesz się wylogować?" lastActiveDate: "Ostatnio użyte w" statusbar: "Pasek stanu" pleaseSelect: "Wybierz opcję" reverse: "Odwróć" colored: "Kolorowe" -refreshInterval: "Okres aktualizacji" label: "Etykieta" type: "Typ" speed: "Prędkość" -slow: "Wolny" -fast: "Szybki" -sensitiveMediaDetection: "Detekcja wrażliwej zawartości" localOnly: "Lokalne tylko" -remoteOnly: "Tylko zdalne instancje" failedToUpload: "Przesyłanie nie powiodło się" cannotUploadBecauseInappropriate: "Nie można przesłać tego pliku, ponieważ jego części zostały wykryte jako potencjalnie nieodpowiednie." cannotUploadBecauseNoFreeSpace: "Przesyłanie nie powiodło się z powodu braku miejsca na dysku." -cannotUploadBecauseExceedsFileSizeLimit: "Nie można przesłać pliku, ponieważ wykracza on poza limit wielkości pliku." beta: "Beta" enableAutoSensitive: "Automatyczne oznaczanie NSFW" enableAutoSensitiveDescription: "Umożliwia automatyczne wykrywanie i oznaczanie zawartości NSFW za pomocą uczenia maszynowego. Nawet jeśli ta opcja jest wyłączona, może być włączona w całej instancji." -activeEmailValidationDescription: "Włącza bardziej restrykcyjną walidację adresów e-mail, co obejmuje sprawdzanie adresów jednorazowych i czy komunikacja z tym adresem jest możliwa. Gdy wyłączone, tylko format adresu e-mail jest sprawdzany." navbar: "Pasek nawigacyjny" -shuffle: "Mieszaj" account: "Konta" move: "Przenieś" pushNotification: "Powiadomienia" @@ -986,74 +875,21 @@ pushNotificationAlreadySubscribed: "Powiadomienia push są włączone" pushNotificationNotSupported: "Przeglądarka lub instancja nie obsługuje powiadomień push" sendPushNotificationReadMessage: "Usuń powiadomienia push po przeczytaniu powiadomień i wiadomości." sendPushNotificationReadMessageCaption: "Chwilowo pojawi się powiadomienie \"{emptyPushNotificationMessage}\". Może wzrosnąć zużycie baterii urządzenia." -windowMaximize: "Maksymalizuj" -windowMinimize: "Minimalizuj" -windowRestore: "Przywróć" -caption: "Legenda" loggedInAsBot: "Jesteś obecnie zalogowany/a jako bot" -tools: "Narzędzia" -cannotLoad: "Nie można wczytać" -numberOfProfileView: "Wyświetlenia profilu" like: "Polub" -unlike: "Usuń polubienie" -numberOfLikes: "Liczba polubień" show: "Wyświetlanie" -neverShow: "Nie pokazuj ponownie" -remindMeLater: "Przypomnij później" -didYouLikeMisskey: "Czy CherryPick się tobie spodobało?" -pleaseDonate: "{host} używa darmowego oprogramowania — CherryPick. Bylibyśmy bardzo wdzięczni za datki, które pozwolą na kontynuację rozwoju CherryPick!" -correspondingSourceIsAvailable: "Odpowiedni kod źródłowy jest dostępny pod {anchor}." -roles: "Role" -role: "Rola" -noRole: "Rola nie znaleziona" -normalUser: "Normalny użytkownik" -undefined: "Niezdefiniowane" -assign: "Przydziel" -unassign: "Cofnij przydzielenie" color: "Kolor" -manageCustomEmojis: "Zarządzaj niestandardowymi Emoji" -manageAvatarDecorations: "Zarządzaj dekoracjami awatara" -invalidParamError: "Błąd parametrów" -permissionDeniedError: "Odrzucono operacje" -permissionDeniedErrorDescription: "Konto nie posiada uprawnień" -preset: "Konfiguracja" -selectFromPresets: "Wybierz konfiguracje" -achievements: "Osiągnięcia" -thisPostMayBeAnnoyingCancel: "Odrzuć" -internalServerError: "Wewnętrzny błąd serwera" -internalServerErrorDescription: "Niespodziewany błąd po stronie serwera" -copyErrorInfo: "Kopiuj informacje o błędzie" -joinThisServer: "Dołącz do chaty" -disableFederationOk: "Wyłącz federacje" -invitationRequiredToRegister: "Ten serwer wymaga zaproszenia. Tylko osoby z zaproszeniem mogą się zarejestrować" -emailNotSupported: "Wysyłanie wiadomości E-mail nie jest obsługiwane na tym serwerze" -postToTheChannel: "Publikuj na kanale" youFollowing: "Śledzeni" icon: "Awatar" replies: "Odpowiedz" renotes: "Udostępnij" -sourceCode: "Kod źródłowy" flip: "Odwróć" -lastNDays: "W ciągu ostatnich {n} dni" -surrender: "Odrzuć" -gameRetry: "Spróbuj ponownie" -_delivery: - stop: "Zawieszono" - _type: - none: "Publikowanie" -_bubbleGame: - _score: - score: "Wynik" _role: - assignTarget: "Przydziel" priority: "Priorytet" _priority: low: "Niski" middle: "Średnie" high: "Wysoki" - _options: - canManageCustomEmojis: "Zarządzaj niestandardowymi Emoji" - canManageAvatarDecorations: "Zarządzaj dekoracjami awatara" _sensitiveMediaDetection: description: "Zmniejsza wysiłek związany z moderacją serwera dzięki automatycznemu rozpoznawaniu zawartości NSFW za pomocą uczenia maszynowego. To nieznacznie zwiększy obciążenie serwera." setSensitiveFlagAutomatically: "Oznacz jako NSFW" @@ -1299,6 +1135,8 @@ _sfx: notification: "Powiadomienia" chat: "Wiadomości" chatBg: "Rozmowy (tło)" + antenna: "Anteny" + channel: "Powiadomienia kanału" _ago: future: "W przyszłości" justNow: "Przed chwilą" @@ -1472,7 +1310,6 @@ _profile: _exportOrImport: allNotes: "Wszystkie wpisy" favoritedNotes: "Ulubione wpisy" - clips: "Klip" followingList: "Obserwowani" muteList: "Wycisz" blockingList: "Zablokuj" @@ -1624,6 +1461,7 @@ _webhookSettings: createWebhook: "Stwórz Webhook" name: "Nazwa" secret: "Sekret" + events: "Uruchomienie Webhooka" active: "Właczono" _events: follow: "Po zaobserwowaniu użytkownika" @@ -1633,12 +1471,6 @@ _webhookSettings: renote: "Po udostępnieniu wpisu" reaction: "Po otrzymaniu reakcji" mention: "Po zostaniu wspomnianym" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Adres e-mail" _moderationLogTypes: suspend: "Zawieś" resetPassword: "Zresetuj hasło" -_reversi: - total: "Łącznie" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 3e22d0f91a..87c78340cb 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -368,8 +368,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Ativar hCaptcha" hcaptchaSiteKey: "Chave do sítio ‘web’" hcaptchaSecretKey: "Chave secreta" -mcaptchaSiteKey: "Chave do sítio ‘web’" -mcaptchaSecretKey: "Chave secreta" recaptcha: "reCAPTCHA" enableRecaptcha: "Habilitar reCAPTCHA" recaptchaSiteKey: "Chave do sítio ‘web’" @@ -733,9 +731,9 @@ reloadToApplySetting: "As configurações serão refletidas após recarregar a p needReloadToApply: "É necessário recarregar a página para refletir as alterações." showTitlebar: "Exibir barra de título" clearCache: "Limpar o cache" -onlineUsersCount: "{n} Pessoas Online" -nUsers: "{n} Usuários" -nNotes: "{n} Notas" +onlineUsersCount: "Pessoas Online" +nUsers: "Usuários" +nNotes: "Notas" sendErrorReports: "Enviar relatórios de erro" sendErrorReportsDescription: "Ao ativar essa opção, informações detalhadas de erro serão compartilhadas com o CherryPick em caso de problemas, o que pode ajudar a melhorar a qualidade do software. As informações de erro podem incluir a versão do sistema operacional, o tipo de navegador e o sua atividade no CherryPick." myTheme: "Meu tema" @@ -767,7 +765,7 @@ emailNotification: "Notificações por e-mail" publish: "Publicar" inChannelSearch: "Pesquisar no canal" useReactionPickerForContextMenu: "Clique com o botão direito do mouse para abrir o seletor de reações." -typingUsers: "{users} pessoas digitando" +typingUsers: "digitando" jumpToSpecifiedDate: "Pular para uma data específica" showingPastTimeline: "Visualizar linha de tempo anterior" clear: "Limpar" @@ -834,7 +832,7 @@ learnMore: "Saiba mais" misskeyUpdated: "CherryPick foi atualizado!" whatIsNew: "Ver atualizações" translate: "Traduzir" -translatedFrom: "Traduzido de {x}" +translatedFrom: "Traduzido de" accountDeletionInProgress: "Encerramento de conta em andamento" usernameInfo: "O nome para identificar exclusivamente a sua conta no servidor. Pode conter letras (az, AZ), números (0~9) e sublinhados (_). O nome de usuário não pode ser alterado posteriormente." aiChanMode: "Modo AI-chan" @@ -1010,12 +1008,6 @@ replies: "Responder" renotes: "Repostar" keepScreenOn: "Manter a tela do dispositivo sempre ligada" flip: "Inversão" -lastNDays: "Últimos {n} dias" -surrender: "Cancelar" -_delivery: - stop: "Suspenso" - _type: - none: "Publicando" _initialAccountSetting: followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo." _serverSettings: @@ -1082,7 +1074,7 @@ _achievements: _login3: title: "Iniciante I" description: "Fez login por um total de 3 dias" - flavor: "De hoje em diante, me chame apenas de Cherrypikist" + flavor: "De hoje em diante, me chame apenas de Misskist" _login7: title: "Iniciante II" description: "Fez login por um total de 7 dias" @@ -1305,8 +1297,8 @@ _preferencesBackups: _channel: featured: "Destaques" following: "Seguindo" - usersCount: "{n} usuários ativos" - notesCount: "{n} notas" + usersCount: "usuários ativos" + notesCount: "notas" nameAndDescription: "Nome e descrição" _menuDisplay: sideFull: "Exibir painel lateral inteiro" @@ -1409,7 +1401,6 @@ _profile: username: "Nome de usuário" _exportOrImport: favoritedNotes: "Notas nos favoritos" - clips: "Clipe" followingList: "Seguindo" muteList: "Silenciar" blockingList: "Bloquear" @@ -1501,12 +1492,6 @@ _webhookSettings: follow: "Quando seguindo um usuário" followed: "Quando sendo seguido" renote: "Quando repostado" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-mail" _moderationLogTypes: suspend: "Suspender" resetPassword: "Redefinir senha" -_reversi: - total: "Total" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 566914a43b..7f6e64cd6d 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -359,8 +359,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Activează hCaptcha" hcaptchaSiteKey: "Site key" hcaptchaSecretKey: "Secret key" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret key" recaptcha: "reCAPTCHA" enableRecaptcha: "Activează reCAPTCHA" recaptchaSiteKey: "Site key" @@ -651,10 +649,6 @@ show: "Arată" icon: "Avatar" replies: "Răspunde" renotes: "Re-notează" -_delivery: - stop: "Suspendat" - _type: - none: "Publicare" _role: _priority: middle: "Mediu" @@ -729,12 +723,6 @@ _deck: mentions: "Mențiuni" _webhookSettings: name: "Nume" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" _moderationLogTypes: suspend: "Suspendă" resetPassword: "Resetează parola" -_reversi: - total: "Total" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 8dbf4654da..0321bcd52e 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -17,7 +17,7 @@ noThankYou: "Нет, спасибо" enterUsername: "Введите имя пользователя" renotedBy: "{user} делится" noNotes: "Нет ни одной заметки" -noNotifications: "Нет уведомлений" +noNotifications: "Нет ни одного уведомления" instance: "Инстанс" settings: "Настройки" notificationSettings: "Настройки уведомлений" @@ -53,8 +53,8 @@ addToAntenna: "Добавить к антенне" sendMessage: "Отправить сообщение" copyRSS: "Скопировать RSS" copyUsername: "Скопировать имя пользователя" -copyUserId: "Скопировать идентификатор пользователя" -copyNoteId: "Скопировать идентификатор заметки" +copyUserId: "Скопировать ID пользователя" +copyNoteId: "Скопировать ID заметки" copyFileId: "Скопировать ID файла" copyFolderId: "Скопировать ID папки" copyProfileUrl: "Скопировать URL профиля " @@ -129,14 +129,13 @@ overwriteFromPinnedEmojis: "Заменить на эмодзи из общего reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»." rememberNoteVisibility: "Запоминать видимость заметок" attachCancel: "Удалить вложение" -deleteFile: "Удалить файл" markAsSensitive: "Отметить как «не для всех»" unmarkAsSensitive: "Снять отметку «не для всех»" enterFileName: "Введите имя файла" mute: "Скрыть" unmute: "Отменить скрытие" -renoteMute: "Скрыть репосты" -renoteUnmute: "Открыть репосты" +renoteMute: "Заглушить репосты" +renoteUnmute: "Включить репосты" block: "Заблокировать" unblock: "Разблокировать" suspend: "Заморозить" @@ -162,8 +161,8 @@ addEmoji: "Добавить эмодзи" settingGuide: "Рекомендуемые настройки" cacheRemoteFiles: "Кешировать внешние файлы" cacheRemoteFilesDescription: "Когда эта настройка отключена, файлы с других сайтов будут загружаться прямо оттуда. Это сэкономит место на сервере, но увеличит трафик, так как не будут создаваться эскизы." -cacheRemoteSensitiveFiles: "Кэшировать внешние файлы «не для всех»" -cacheRemoteSensitiveFilesDescription: "Если отключено, файлы «не для всех» загружаются непосредственно с удалённых серверов, не кэшируясь." +cacheRemoteSensitiveFiles: "Кешировать внешние файлы" +cacheRemoteSensitiveFilesDescription: "Описание удаленных внешних файлов в кэше" flagAsBot: "Аккаунт бота" flagAsBotDescription: "Включите, если этот аккаунт управляется программой. Это позволит системе CherryPick учитывать это, а также поможет разработчикам других ботов предотвратить бесконечные циклы взаимодействия." flagAsCat: "Аккаунт кота" @@ -262,7 +261,6 @@ removed: "Удалено" removeAreYouSure: "Хотите удалить «{x}»?" deleteAreYouSure: "Хотите удалить «{x}»?" resetAreYouSure: "На самом деле сбросить?" -areYouSure: "Вы уверены?" saved: "Сохранено" messaging: "Сообщения" upload: "Загрузить" @@ -280,7 +278,7 @@ noMoreHistory: "История закончилась" startMessaging: "Начать общение" nUsersRead: "Прочитали {n}" agreeTo: "Я соглашаюсь с {0}" -agree: "Согласен" +agree: "Согласиться" agreeBelow: "Согласен со следующими" basicNotesBeforeCreateAccount: "Записи, перед созданием аккаунта" termsOfService: "Условия использования" @@ -313,7 +311,6 @@ folderName: "Имя папки" createFolder: "Создать папку" renameFolder: "Переименовать папку" deleteFolder: "Удалить папку" -folder: "Папка" addFile: "Добавить файл" emptyDrive: "Диск пуст" emptyFolder: "Папка пуста" @@ -327,7 +324,7 @@ copyUrl: "Копировать ссылку" rename: "Переименовать" avatar: "Аватар" banner: "Шапка" -displayOfSensitiveMedia: "Отображение содержимого не для всех" +displayOfSensitiveMedia: "Определение деликатного контента" whenServerDisconnected: "Когда соединение с сервером потеряно" disconnectedFromServer: "Разорвано соединение с сервером" reload: "Перезагрузить" @@ -375,10 +372,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Включить hCaptcha" hcaptchaSiteKey: "Ключ сайта" hcaptchaSecretKey: "Секретный ключ" -mcaptcha: "mCaptcha" -enableMcaptcha: "Включить mCaptcha" -mcaptchaSiteKey: "Ключ сайта" -mcaptchaSecretKey: "Секретный ключ" recaptcha: "reCAPTCHA" enableRecaptcha: "Включить reCAPTCHA" recaptchaSiteKey: "Ключ сайта" @@ -420,7 +413,7 @@ about: "Описание" aboutMisskey: "О CherryPick" administrator: "Администратор" token: "Токен" -2fa: "Двухфакторная аутентификация" +2fa: "2-х факторная аутентификация" setupOf2fa: "Настроить двухфакторную аутентификацию" totp: "Приложение-аутентификатор" totpDescription: "Описание приложения-аутентификатора" @@ -496,7 +489,7 @@ native: "Системные" disableDrawer: "Не использовать выдвижные меню" youHaveNoGroups: "У вас нет ни одной группы" joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные" -showNoteActionsOnlyHover: "Показывать кнопки у заметок только при наведении" +showNoteActionsOnlyHover: "Показывать кнопки управления заметкой только при наведении" noHistory: "История пока пуста" signinHistory: "Журнал посещений" enableAdvancedMfm: "Включить расширенный MFM" @@ -509,8 +502,8 @@ createAccount: "Новая учётная запись" existingAccount: "Существующая учётная запись" regenerate: "Создать повторно" fontSize: "Размер шрифта" -mediaListWithOneImageAppearance: "Вид изображения, если оно единственное в списке" -limitTo: "Ограничить до {x}" +mediaListWithOneImageAppearance: "Показывать список медиа только одним изображением" +limitTo: "Обрезать до {x}" noFollowRequests: "Нерассмотренные запросы на подписку отсутствуют" openImageInNewTab: "Открыть изображение в новой вкладке" dashboard: "Панель управления" @@ -544,7 +537,7 @@ objectStorageUseSSLDesc: "Отключите, если не собираетес objectStorageUseProxy: "Использовать прокси" objectStorageUseProxyDesc: "Отключите, если не будете испоьзовать прокси для соединений по протоколу ObjectStorage." objectStorageSetPublicRead: "Устанавливать public-read при загрузке на сервер" -s3ForcePathStyleDesc: "Включение s3ForcePathStyle приводит к тому, что имя корзины указывается как часть пути в URL, а не в имени хоста. Может потребоваться включить при использовании локального Minio или чего-то подобного." +s3ForcePathStyleDesc: "Включение s3ForcePathStyle принудительно указывает имя корзины как часть пути в URL-адресе вместо имени хоста. Может потребоваться активация при использовании таких вещей, как локальный Minio." serverLogs: "Журнал сервера" deleteAll: "Удалить всё" showFixedPostForm: "Показывать поле для ввода новой заметки наверху ленты" @@ -558,8 +551,6 @@ showInPage: "Показать страницу" popout: "Развернуть" volume: "Громкость" masterVolume: "Основная регулировка громкости" -notUseSound: "Выключить звук" -useSoundOnlyWhenActive: "Использовать звук, когда Misskey активен." details: "Подробнее" chooseEmoji: "Выберите эмодзи" unableToProcess: "Не удаётся завершить операцию" @@ -580,10 +571,6 @@ output: "Выходы" script: "Скрипт" disablePagesScript: "Отключить скрипты на «Страницах»" updateRemoteUser: "Обновить данные пользователя с его сервера" -unsetUserAvatar: "Убрать аватар" -unsetUserAvatarConfirm: "Вы точно хотите убрать аватар?" -unsetUserBanner: "Убрать баннер" -unsetUserBannerConfirm: "Вы точно хотите убрать баннер?" deleteAllFiles: "Удалить все файлы" deleteAllFilesConfirm: "Вы хотите удалить все файлы?" removeAllFollowing: "Удалить всех подписчиков" @@ -594,7 +581,7 @@ yourAccountSuspendedTitle: "Эта учетная запись заблокир yourAccountSuspendedDescription: "Эта учетная запись была заблокирована из-за нарушения условий предоставления услуг сервера. Свяжитесь с администратором, если вы хотите узнать более подробную причину. Пожалуйста, не создавайте новую учетную запись." tokenRevoked: "Токен недействителен" tokenRevokedDescription: "Срок действия вашего токена входа истек. Пожалуйста, войдите снова." -accountDeleted: "Учетная запись удалена" +accountDeleted: "Эта учетная запись удалена" accountDeletedDescription: "Эта учетная запись удалена" menu: "Меню" divider: "Линия-разделитель" @@ -634,7 +621,6 @@ medium: "Средне" small: "Мелко" generateAccessToken: "Создать токен доступа" permission: "Разрешения" -adminPermission: "Доступ администратора" enableAll: "Включить все" disableAll: "Выключить всё" tokenRequested: "Открыть доступ к учётной записи" @@ -656,7 +642,6 @@ smtpSecure: "Использовать SSL/TLS для SMTP-соединений" smtpSecureInfo: "Выключите при использовании STARTTLS." testEmail: "Проверка доставки электронной почты" wordMute: "Скрытие слов" -hardWordMute: "" regexpError: "Ошибка в регулярном выражении" regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:" instanceMute: "Глушение инстансов" @@ -678,7 +663,6 @@ useGlobalSettingDesc: "Если включено, будут использов other: "Другие" regenerateLoginToken: "Создать новый токен для входа" regenerateLoginTokenDescription: "Создаёт новый токен, используемый внутри программы во время входа. Обычно в этом нет необходимости. При создании все устройства будут отключены." -theKeywordWhenSearchingForCustomEmoji: "Это ключевое слово будет использовано при поиске эмодзи." setMultipleBySeparatingWithSpace: "Можно написать несколько через пробел" fileIdOrUrl: "Идентификатор файла или ссылка" behavior: "Поведение" @@ -749,7 +733,7 @@ useSystemFont: "Использовать шрифт, предлагаемый с clips: "Подборки" experimentalFeatures: "Экспериментальные функции" experimental: "Экспериментальные" -thisIsExperimentalFeature: "Это экспериментальная функция. Её поведение, вероятно, поменяется в следующей версии, а ещё она может работать не так, как задумано." +thisIsExperimentalFeature: "Это экспериментальная функция. Технические характеристики могут измениться или он может работать неправильно." developer: "Разработчик" makeExplorable: "Опубликовать профиль в «Обзоре»." makeExplorableDescription: "Если выключить, ваш профиль не будет показан в разделе «Обзор»." @@ -834,7 +818,7 @@ noMaintainerInformationWarning: "Не заполнены сведения об noBotProtectionWarning: "Ботозащита не настроена" configure: "Настроить" postToGallery: "Опубликовать в галерею" -postToHashtag: "Написать заметку с этим хэштегом" +postToHashtag: "Опубликовать пост с этим хештегом" gallery: "Галерея" recentPosts: "Недавние публикации" popularPosts: "Популярные публикации" @@ -863,7 +847,7 @@ useBlurEffect: "Размытие в интерфейсе" learnMore: "Подробнее" misskeyUpdated: "CherryPick обновился!" whatIsNew: "Что новенького?" -translate: "Перевести" +translate: "Перевод" translatedFrom: "Перевод. Язык оригинала — {x}" accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи" usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже." @@ -875,11 +859,11 @@ lastCommunication: "Последнее сообщение" resolved: "Решено" unresolved: "Без решения" breakFollow: "Отписка" -breakFollowConfirm: "Действительно удалить этого подписчика?" +breakFollowConfirm: "Удалить из подписок пользователя ?" itsOn: "Включено" itsOff: "Выключено" -on: "Вкл." -off: "Выкл." +on: "Вкл" +off: "Выкл" emailRequiredForSignup: "Для регистрации учётной записи нужен адрес электронной почты" unread: "Непрочитанное" filter: "Фильтры" @@ -910,7 +894,7 @@ numberOfColumn: "Количество столбцов" searchByGoogle: "Поиск" instanceDefaultLightTheme: "Светлая тема по умолчанию" instanceDefaultDarkTheme: "Темная тема по умолчанию" -instanceDefaultThemeDescription: "Введите код темы в формате объекта." +instanceDefaultThemeDescription: "Описание темы по умолчанию для инстанса" mutePeriod: "Продолжительность скрытия" period: "Опрос длится" indefinitely: "вечно" @@ -934,7 +918,7 @@ thereIsUnresolvedAbuseReportWarning: "Остались нерешённые жа recommended: "Рекомендуем" check: "Проверить" driveCapOverrideLabel: "Изменение лимита дискового пространства для этого пользователя" -driveCapOverrideCaption: "Введите нуль или меньше, чтобы использовать значение по умолчанию." +driveCapOverrideCaption: "Укажите меньше или равное нулю для отмены" requireAdminForView: "Для просмотра необходимо иметь аккаунт администратора" isSystemAccount: "Данная учётная запись создана автоматически и управляется системой" typeToConfirm: "Введите {x} для продолжения" @@ -954,7 +938,7 @@ type: "Тип" speed: "Скорость" slow: "Медленная" fast: "Быстрая" -sensitiveMediaDetection: "Распознание содержимого не для всех" +sensitiveMediaDetection: "Определение содержимого деликатного характера" localOnly: "Локально" remoteOnly: "Только удалённо" failedToUpload: "Сбой выгрузки" @@ -1031,17 +1015,15 @@ invitationRequiredToRegister: "Этот сервер в настоящее вр emailNotSupported: "Доставка почты не поддерживается на этом сервере" postToTheChannel: "Отправить в канал" cannotBeChangedLater: "Это нельзя изменить позже" -reactionAcceptance: "Допустимые реакции" -likeOnly: "Только «нравится!»" -likeOnlyForRemote: "Всё (с других серверов только «нравится!»)" -nonSensitiveOnly: "Только безопасные" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "Только безопасные (с других серверов только «нравится!»)" +reactionAcceptance: "Принятие реакций" +likeOnly: "Только лайки" +likeOnlyForRemote: "Только лайки с удалённых серверов" +nonSensitiveOnly: "Безопасный серфинг" rolesAssignedToMe: "Мои роли" resetPasswordConfirm: "Сбросить пароль?" sensitiveWords: "Чувствительные слова" sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк." sensitiveWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение." -prohibitedWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение." notesSearchNotAvailable: "Поиск заметок недоступен" license: "Лицензия" unfavoriteConfirm: "Удалить избранное?" @@ -1056,20 +1038,20 @@ noteIdOrUrl: "ID или ссылка на заметку" video: "Видео" videos: "Видео" dataSaver: "Экономия трафика" -accountMigration: "Перенос учётной записи" -accountMoved: "Учётная запись перенесена" +accountMigration: "Перенести учётную запись" +accountMoved: "Учетная запись перенесена" accountMovedShort: "Эта учётная запись перемещена" -operationForbidden: "Это действие запрещено" +operationForbidden: "Эта операция невозможна." forceShowAds: "Всегда отображать рекламу" -addMemo: "Добавить памятку" -editMemo: "Изменить памятку" -reactionsList: "Список реакций" +addMemo: "Добавить заметку" +editMemo: "Редактировать заметку" +reactionsList: "Реакции" renotesList: "Репосты" -notificationDisplay: "Отображение уведомлений" -leftTop: "Влево вверх" -rightTop: "Вправо вверх" -leftBottom: "Влево вниз" -rightBottom: "Вправо вниз" +notificationDisplay: "Отображение уведомления" +leftTop: "Верхний левый угол" +rightTop: "Сверху справа" +leftBottom: "Снизу слева" +rightBottom: "Снизу справа" vertical: "Вертикальная" horizontal: "Сбоку" position: "Позиция" @@ -1108,15 +1090,7 @@ icon: "Аватар" replies: "Ответы" renotes: "Репост" loadReplies: "Показать ответы" -sourceCode: "Исходный код" flip: "Переворот" -code: "Код" -lastNDays: "Последние {n} сут" -surrender: "Этот пост не может быть отменен." -_delivery: - stop: "Заморожено" - _type: - none: "Публикация" _initialAccountSetting: accountCreated: "Аккаунт успешно создан!" letsStartAccountSetup: "Давайте настроим вашу учётную запись." @@ -1190,7 +1164,7 @@ _achievements: _login3: title: "Новичок Ⅰ" description: "3 дня на сайте" - flavor: "С сегодняшнего дня зовите меня просто Черрипикиец" + flavor: "С сегодняшнего дня зовите меня просто мискиец" _login7: title: "Новичок Ⅱ" description: "Неделя на сайте" @@ -1697,6 +1671,8 @@ _sfx: notification: "Уведомления" chat: "Сообщения" chatBg: "Сообщения (фон)" + antenna: "Антенна" + channel: "Канал" _ago: future: "Из будущего" justNow: "Только что" @@ -1726,6 +1702,7 @@ _2fa: registerTOTP: "Начните настраивать приложение-аутентификатор" step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." + step2Click: "Нажав на QR-код, вы можете зарегистрироваться с помощью приложения для аутентификации или брелка для ключей, установленного на вашем устройстве." step3Title: "Введите проверочный код" step3: "И наконец, введите код, который покажет приложение." step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом." @@ -1802,7 +1779,7 @@ _weekday: _widgets: profile: "Профиль" instanceInfo: "Информация об инстансе" - memo: "Памятки" + memo: "Напоминания" notifications: "Уведомления" timeline: "Лента" calendar: "Календарь" @@ -1893,7 +1870,6 @@ _profile: _exportOrImport: allNotes: "Все заметки\n" favoritedNotes: "Избранное" - clips: "Подборка" followingList: "Подписки" muteList: "Скрытые" blockingList: "Заблокированные" @@ -2070,15 +2046,6 @@ _webhookSettings: createWebhook: "Создать вебхук" name: "Название" active: "Вкл." -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Электронная почта" _moderationLogTypes: suspend: "Заморозить" - addCustomEmoji: "Добавлено эмодзи" - updateCustomEmoji: "Изменено эмодзи" - deleteCustomEmoji: "Удалено эмодзи" resetPassword: "Сброс пароля:" -_reversi: - total: "Всего" diff --git a/locales/si-LK.yml b/locales/si-LK.yml index e130d68ed8..ed97d539c0 100644 --- a/locales/si-LK.yml +++ b/locales/si-LK.yml @@ -1,19 +1 @@ --- -_lang_: "සිංහල" -monthAndDay: "{month}-{day}" -username: "පරිශීලක නාමය" -password: "මුරපදය" -cancel: "අවලංගු කරන්න" -instance: "සර්වර්" -login: "පිවිසෙන්න" -users: "පරිශීලක" -note: "නෝට්" -notes: "නෝට්" -instances: "සර්වර්" -smtpUser: "පරිශීලක නාමය" -smtpPass: "මුරපදය" -user: "පරිශීලක" -_sfx: - note: "නෝට්" -_profile: - username: "පරිශීලක නාමය" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 8f40be67fe..e84bf3bab0 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -349,8 +349,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Zapnúť hCaptchu" hcaptchaSiteKey: "Site key" hcaptchaSecretKey: "Secret key" -mcaptchaSiteKey: "Site key" -mcaptchaSecretKey: "Secret key" recaptcha: "reCAPTCHA" enableRecaptcha: "Zapnúť ReCAPTCHA" recaptchaSiteKey: "Site key" @@ -933,13 +931,7 @@ youFollowing: "Sledované" icon: "Avatar" replies: "Odpovedať" renotes: "Preposlať" -sourceCode: "Zdrojový kód" flip: "Preklopiť" -lastNDays: "Posledných {n} dní" -_delivery: - stop: "Zmrazené" - _type: - none: "Zverejňovanie" _role: priority: "Priorita" _priority: @@ -1206,6 +1198,8 @@ _sfx: notification: "Oznámenia" chat: "Chat" chatBg: "Chat (pozadie)" + antenna: "Antény" + channel: "Upozornenia kanála" _ago: future: "Budúcnosť" justNow: "Teraz" @@ -1373,7 +1367,6 @@ _profile: changeBanner: "Zmeniť banner" _exportOrImport: allNotes: "Všetky poznámky" - clips: "Klip" followingList: "Sledujete" muteList: "Vypnúť zvuk" blockingList: "Zablokovať" @@ -1531,12 +1524,6 @@ _deck: _webhookSettings: name: "Názov" active: "Zapnuté" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" _moderationLogTypes: suspend: "Zmraziť" resetPassword: "Resetovať heslo" -_reversi: - total: "Celkom" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index fe941dae4d..87064f78bc 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -345,8 +345,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Aktivera hCaptcha" hcaptchaSiteKey: "Webbplatsnyckel" hcaptchaSecretKey: "Hemlig nyckel" -mcaptchaSiteKey: "Webbplatsnyckel" -mcaptchaSecretKey: "Hemlig nyckel" recaptcha: "reCAPTCHA" enableRecaptcha: "Aktivera reCAPTCHA" recaptchaSiteKey: "Webbplatsnyckel" @@ -488,10 +486,6 @@ dataSaver: "Databesparing" icon: "Profilbild" replies: "Svara" renotes: "Omnotera" -_delivery: - stop: "Suspenderad" - _type: - none: "Publiceras" _achievements: _types: _open3windows: @@ -513,6 +507,7 @@ _sfx: note: "Noter" notification: "Notifikationer" chat: "Chatt" + antenna: "Antenner" _2fa: renewTOTPCancel: "Nej tack" _antennaSources: @@ -577,10 +572,6 @@ _deck: _webhookSettings: name: "Namn" active: "Aktiverad" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-post" _moderationLogTypes: suspend: "Suspendera" resetPassword: "Återställ Lösenord" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 73d49579e2..dfbe4ce63f 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1,24 +1,24 @@ --- _lang_: "ภาษาไทย" -headlineMisskey: "เชื่อมต่อเครือข่ายโดยโน้ต" -introMisskey: "ยินดีต้อนรับทุกคนจ้า! CherryPick คือ ซอฟต์แวร์โอเพนซอร์สสำหรับบริการไมโครบล็อกกิ้ง (MicroBlogging) แบบกระจายศูนย์อำนาจ (Decentralized) \n\nเขียน “โน้ต (Note)” เพื่อส่งต่อเรื่องราวของคุณให้ทั้งโลกได้รับรู้📡\nและอย่าลืมที่จะ “รีแอคชั่น” กับเรื่องราวของคนอื่น ๆ ด้วยนะ! 👍\n\nท่องสำรวจโลกใบใหม่กันเถอะ🚀" -poweredByMisskeyDescription: "{name} เป็นหนึ่งในเซิร์ฟเวอร์ของแพลตฟอร์มโอเพ่นซอร์ส CherryPick" +headlineMisskey: "เชื่อมต่อระบบ Network ด้วย Note" +introMisskey: "ยินดีต้อนรับทุกคนจ้า! CherryPick คือ บริการไมโครบล็อกกิ้ง (MicroBlogging) แบบกระจายศูนย์อำนาจ (Decentralized) \n\nเขียน \"โน้ต (Note)\" เพื่อส่งต่อเรื่องราวของคุณให้ทั้งโลกได้รับรู้📡\nและอย่าลืมที่จะ \"React\" กับเรื่องราวของคนอื่น ๆ ด้วย! 👍\n\nมุ่งสู่โลกใบใหม่กันเถอะ🚀" +poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส CherryPick (เรียกว่า \"อินสแตนซ์ CherryPick\")" monthAndDay: "{month}/{day}" search: "ค้นหา" -notifications: "เเจ้งเตือน" +notifications: "การเเจ้งเตือน" username: "ชื่อผู้ใช้" password: "รหัสผ่าน" -forgotPassword: "ลืมรหัสผ่าน" -fetchingAsApObject: "กำลังดึงข้อมูลจากสหพันธ์..." -ok: "ตกลง" +forgotPassword: "ลืมรหัสผ่านใช่ไหม" +fetchingAsApObject: "กำลังดึงข้อมูล จาก เฟดิเวิร์ส..." +ok: "โอเค" gotIt: "เข้าใจแล้ว !" cancel: "ยกเลิก" -noThankYou: "ไม่เอาดีกว่า" -enterUsername: "กรอกชื่อผู้ใช้" +noThankYou: "ไม่เป็นไร" +enterUsername: "ใส่ชื่อผู้ใช้" renotedBy: "รีโน้ตโดย {user}" noNotes: "ไม่มีโน้ต" noNotifications: "ไม่มีการแจ้งเตือน" -instance: "เซิร์ฟเวอร์" +instance: "อินสแตนซ์" settings: "การตั้งค่า" notificationSettings: "ตั้งค่าการแจ้งเตือน" basicSettings: "การตั้งค่าพื้นฐาน" @@ -26,30 +26,30 @@ otherSettings: "การตั้งค่าอื่นๆ" openInWindow: "เปิดในหน้าต่าง" profile: "โปรไฟล์" timeline: "ไทม์ไลน์" -noAccountDescription: "ผู้ใช้รายนี้ยังไม่ได้เขียนคำแนะนำตัว" +noAccountDescription: "ผู้ใช้รายนี้ยังไม่ได้เขียนลงประวัติของพวกเขา" login: "เข้าสู่ระบบ" loggingIn: "กำลังเข้าสู่ระบบ" logout: "ออกจากระบบ" signup: "สร้างบัญชีผู้ใช้" -uploading: "กำลังอัปโหลด" +uploading: "กำลังอัพโหลด..." save: "บันทึก" -users: "ผู้ใช้" +users: "ผู้ใช้งาน" addUser: "เพิ่มผู้ใช้" favorite: "รายการโปรด" favorites: "รายการโปรด" unfavorite: "ลบออกจากรายการโปรด" -favorited: "เพิ่มลงรายการโปรดแล้ว" -alreadyFavorited: "เพิ่มลงรายการโปรดอยู่แล้ว" -cantFavorite: "ไม่สามารถเพิ่มลงรายการโปรดได้" -pin: "ปักหมุด" -unpin: "เลิกปักหมุด" +favorited: "เพิ่มแล้วในรายการโปรด" +alreadyFavorited: "เพิ่มในรายการโปรดอยู่แล้ว" +cantFavorite: "ไม่สามารถเพิ่มในรายการโปรดได้" +pin: "ปักหมุดไปยังโปรไฟล์" +unpin: "เลิกปักหมุดจากโปรไฟล์" copyContent: "คัดลอกเนื้อหา" copyLink: "คัดลอกลิงก์" copyLinkRenote: "คัดลอกลิงก์รีโน้ต" delete: "ลบ" deleteAndEdit: "ลบและแก้ไข" -deleteAndEditConfirm: "ต้องการลบโน้ตนี้และแก้ไขใหม่ใช่ไหม? รีแอคชั่น รีโน้ต และการตอบกลับต่อโน้ตนี้ทั้งหมดจะถูกลบออกด้วย" -addToList: "เพิ่มลงรายชื่อ" +deleteAndEditConfirm: "นายแน่ใจแล้วเหรอ? ว่าต้องการลบโน้ตนี้และแก้ไข คุณอาจจะสูญเสียการโต้ตอบ, โน้ต, และการตอบกลับทั้งหมดได้นะ" +addToList: "เพิ่มในลิสต์" addToAntenna: "เพิ่มไปยังเสาอากาศ" sendMessage: "ส่งข้อความ" copyRSS: "คัดลอก RSS" @@ -59,189 +59,168 @@ copyNoteId: "คัดลอก ID โน้ต " copyFileId: "คัดลอกไฟล์ ID" copyFolderId: "คัดลอกโฟลเดอร์ ID" copyProfileUrl: "คัดลอกโปรไฟล์ URL" -searchUser: "ค้นหาผู้ใช้" -searchThisUsersNotes: "ค้นหาโน้ตของผู้ใช้" +searchUser: "ค้นหาผู้ใช้งาน" reply: "ตอบกลับ" -loadMore: "แสดงเพิ่มเติม" +loadMore: "โหลดเพิ่มเติม" showMore: "แสดงเพิ่มเติม" showLess: "ปิด" youGotNewFollower: "ได้ติดตามคุณ" -receiveFollowRequest: "มีคำขอติดตามส่งมาหา" -followRequestAccepted: "การติดตามได้รับการอนุมัติแล้ว" +receiveFollowRequest: "คำขอผู้ติดตามที่ได้รับ" +followRequestAccepted: "ผู้ติดตามได้ตอบรับคำขอร้องของคุณแล้ว" mention: "กล่าวถึง" -mentions: "กล่าวถึงคุณ" -directNotes: "โพสต์แบบไดเร็กต์" +mentions: "พูดถึง" +directNotes: "ไดเร็คโน้ต" importAndExport: "นำเข้า / ส่งออก" import: "นำเข้า" -export: "ส่งออก" +export: "นำออก" files: "ไฟล์" download: "ดาวน์โหลด" -driveFileDeleteConfirm: "ต้องการลบไฟล์ “{name}” ใช่ไหม? โน้ตที่แนบมากับไฟล์นี้ก็จะถูกลบไปด้วย" -unfollowConfirm: "ต้องการเลิกติดตาม {name} ใช่ไหม?" -exportRequested: "คุณได้ร้องขอการส่งออก อาจใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว" -importRequested: "คุณได้ร้องขอการนำเข้า การดำเนินการนี้อาจใช้เวลาสักครู่" -lists: "รายชื่อ" -noLists: "คุณไม่มีรายชื่อใดๆ" +driveFileDeleteConfirm: "คุณต้องการลบไฟล์ \"{name}\" ใช่หรือไม่? โน้ตย่อที่แนบมากับไฟล์นี้ก็จะถูกลบไปด้วย" +unfollowConfirm: "นายแน่ใจแล้วหรอว่าต้องการเลิกติดตาม {name}?" +exportRequested: "เมื่อคุณได้ร้องขอการส่งออก อาจจะต้องใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว" +importRequested: "เมื่อคุณได้ร้องขอการนำเข้า อาจจะต้องใช้เวลาสักครู่นะ" +lists: "รายการ" +noLists: "คุณไม่มีลิสต์ใด ๆ" note: " โน้ต" -notes: " โน้ต" +notes: "ตัวโน้ต" following: "กำลังติดตาม" followers: "ผู้ติดตาม" followsYou: "ติดตามคุณ" -createList: "สร้างรายชื่อ" -manageLists: "จัดการรายชื่อ" +createList: "สร้างลิสต์" +manageLists: "จัดการลิสต์" error: "ผิดพลาด!" somethingHappened: "อุ๊ย ! มีอะไรบางอย่างผิดพลาด" retry: "ลองใหม่อีกครั้ง" pageLoadError: "เกิดข้อผิดพลาดในการโหลดหน้านี้" -pageLoadErrorDescription: "ปัญหานี้มักเกิดจากแคชของเครือข่ายหรือเบราว์เซอร์ ควรล้างแคช, รอสักครู่ แล้วลองใหม่อีกครั้ง" +pageLoadErrorDescription: "โดยปกติแล้วมักจะเกิดจากข้อผิดพลาดของเครือข่ายหรือแคชของเบราว์เซอร์ ลองล้างแคชแล้วลองใหม่อีกครั้งหลังจากรอสักครู่ " serverIsDead: "เซิร์ฟเวอร์นี้ไม่มีการตอบสนอง โปรดกรุณารอสักครู่แล้วลองใหม่อีกครั้ง" youShouldUpgradeClient: "หากต้องการดูหน้านี้ กรุณาโหลดหน้าใหม่เพื่ออัปเดตไคลเอ็นต์ของคุณ" -enterListName: "ป้อนนามเรียกของรายชื่อชุดนี้" +enterListName: "ใส่ชื่อสำหรับรายการลิสต์" privacy: "ความเป็นส่วนตัว" -makeFollowManuallyApprove: "อนุมัติคำขอติดตามด้วยตนเอง" +makeFollowManuallyApprove: "ติดตามคำขอที่ต้องได้รับการอนุมัติ" defaultNoteVisibility: "การมองเห็นที่เป็นค่าเริ่มต้น" follow: "ติดตาม" followRequest: "ส่งคำขอติดตาม" followRequests: "ส่งคำขอติดตาม" unfollow: "เลิกติดตาม" -followRequestPending: "รออนุมัติคำขอติดตาม" -enterEmoji: "พิมพ์เอโมจิ" +followRequestPending: "กำลังรอดำเนินการร้องขอติดตาม" +enterEmoji: "ใส่อีโมจิ" renote: "รีโน้ต" unrenote: "เลิกรีโน้ต" renoted: "รีโน้ตแล้ว" -renotedToX: "รีโน้ตให้ {name} แล้ว" -cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตใหม่ได้" -cantReRenote: "รีโน้ตไม่สามารถรีโน้ตซ้ำได้" +cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตไว้ใหม่ได้นะ" +cantReRenote: "ไม่สามารถรีโน้ตเอาไว้ใหม่ได้นะ" quote: "อ้างอิง" -inChannelRenote: "รีโน้ตในช่องเท่านั้น" -inChannelQuote: "อ้างอิงในช่องเท่านั้น" -renoteToChannel: "รีโน้ตไปที่ช่อง" -renoteToOtherChannel: "รีโน้ตไปยังช่องอื่น" -pinnedNote: "โน้ตที่ปักหมุดไว้" -pinned: "ปักหมุด" +inChannelRenote: "รีโน้ตช่องแชลแนลเท่านั้น" +inChannelQuote: "อ้างช่องเท่านั้น" +pinnedNote: "โน้ตที่ปักหมุดเอาไว้" +pinned: "ปักหมุดไปยังโปรไฟล์" you: "คุณ" clickToShow: "คลิกเพื่อแสดง" -sensitive: "เนื้อหาที่ละเอียดอ่อน" +sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW" add: "เพิ่ม" reaction: "รีแอคชั่น" reactions: "รีแอคชั่น" -emojiPicker: "ตัวจิ้มเอโมจิ" -pinnedEmojisForReactionSettingDescription: "ตรึงเอโมจิไว้ด้านบนสำหรับรีแอคชั่นอย่างเร่งด่วน" -pinnedEmojisSettingDescription: "ตรึงเอโมจิไว้ด้านบนสำหรับพิมพ์เอโมจิอย่างเร่งด่วน" -emojiPickerDisplay: "แสดงตัวจิ้มเอโมจิ" -overwriteFromPinnedEmojisForReaction: "เขียนทับการตั้งค่ารีแอคชั่น" -overwriteFromPinnedEmojis: "เขียนทับการตั้งค่าทั่วไป" -reactionSettingDescription2: "ลากเพื่อจัดลำดับใหม่ คลิกที่เอโมจินั้นเพื่อลบ กด “+” เพื่อเพิ่ม" -rememberNoteVisibility: "จำการตั้งค่าการมองเห็นโน้ต" -attachCancel: "ยกเลิกแนบไฟล์" -deleteFile: "ลบไฟล์ออก" -markAsSensitive: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน" -unmarkAsSensitive: "ยกเลิกทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน" +reactionSettingDescription2: "กดลากเพื่อจัดลำดับใหม่ กดคลิกเพื่อลบ กด \"+\" เพื่อเพิ่ม" +rememberNoteVisibility: "จดจำการตั้งค่าการมองเห็นตัวโน้ต" +attachCancel: "ลบไฟล์ออกที่แนบมา" +markAsSensitive: "ทำเครื่องหมายว่าละเอียดอ่อน" +unmarkAsSensitive: "ยกเลิกทำเครื่องหมายเป็น NSFW" enterFileName: "พิมพ์ชื่อไฟล์" mute: "ปิดเสียง" unmute: "ยกเลิกการปิดเสียง" renoteMute: "ปิดเสียงรีโน้ต" renoteUnmute: "เปิดเสียง รีโน้ต" -block: "บล็อก" -unblock: "เลิกบล็อก" -suspend: "ระงับ" -unsuspend: "เลิกระงับ" -blockConfirm: "ต้องการบล็อกบัญชีนี้ใช่ไหม?" -unblockConfirm: "ต้องการเลิกบล็อกบัญชีนี้ใช่ไหม?" -suspendConfirm: "ต้องการระงับบัญชีนี้ใช่ไหม?" -unsuspendConfirm: "ต้องการยกเลิกการระงับบัญชีนี้ใช่ไหม?" -selectList: "เลือกรายชื่อ" -editList: "แก้ไขรายชื่อ" -selectChannel: "เลือกช่อง" +block: "บล็อค" +unblock: "เลิกปิดกั้น" +suspend: "ถูกระงับ" +unsuspend: "ยกเลิกระงับ" +blockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต้องการบล็อกบัญชีนี้" +unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต้องการปลดบล็อคบัญชีนี้" +suspendConfirm: "แน่ใจว่าคุณต้องการระงับบัญชีนี้?" +unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้" +selectList: "เลือกรายการ" +editList: "แก้ไขรายการ" +selectChannel: "เลือกแชนแนล" selectAntenna: "เลือกเสาอากาศ" editAntenna: "แก้ไขเสาอากาศ" -createAntenna: "สร้างเสาอากาศ" selectWidget: "เลือกวิดเจ็ต" editWidgets: "แก้ไขวิดเจ็ต" editWidgetsExit: "เรียบร้อย" -customEmojis: "เอโมจิที่กำหนดเอง" -emoji: "เอโมจิ" +customEmojis: "กำหนดอีโมจิเอง" +emoji: "อีโมจิ" emojis: "อีโมจิ" -emojiName: "ชื่อเอโมจิ" -emojiUrl: "URL ของเอโมจิ" -addEmoji: "แทรกเอโมจิ" +emojiName: "ชื่ออิโมจิ" +emojiUrl: "อิโมจิ URL" +addEmoji: "แทรกอีโมจิ" settingGuide: "การตั้งค่าที่แนะนำ" cacheRemoteFiles: "แคชไฟล์ระยะไกล" -cacheRemoteFilesDescription: "หากเปิดใช้งาน ไฟล์ระยะไกลจะถูกแคชไว้ ทำให้แสดงภาพเร็วขึ้น แต่ก็ใช้พื้นที่เก็บข้อมูลของเซิร์ฟเวอร์มากขึ้นเช่นกัน สำหรับขีดจำกัดที่ผู้ใช้ระยะไกลถูกแคชไว้จะขึ้นอยู่กับความจุไดรฟ์ตามบทบาทของเขา เมื่อเกินแล้วไฟล์เก่าจะถูกลบออกและเก็บเป็นลิงก์แทน หากปิดใช้งาน ไฟล์ระยะไกลจะถูกเก็บเป็นลิงก์ตั้งแต่ต้น เราแนะนำให้ตั้งค่า proxyRemoteFiles ใน default.yml เป็น true เพื่อสร้างธัมบ์เนลและปกป้องความเป็นส่วนตัวของผู้ใช้" -youCanCleanRemoteFilesCache: "สามารถลบแคชทั้งหมดได้โดยใช้ปุ่ม 🗑️ ในหน้าการจัดการไฟล์" -cacheRemoteSensitiveFiles: "แคชไฟล์ระยะไกลที่มีเนื้อหาละเอียดอ่อน" -cacheRemoteSensitiveFilesDescription: "เมื่อปิดการใช้งานการตั้งค่านี้ ไฟล์ระยะไกลที่มีเนื้อหาละเอียดอ่อนจะถูกโหลดโดยตรงจากเซิร์ฟเวอร์ระยะไกลโดยไม่มีการแคช" -flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอต" -flagAsBotDescription: "เปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยโปรแกรม เมื่อเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นในการป้องกันการสร้างห่วงโซ่การโต้ตอบแบบอนันต์กับบอตตัวอื่น และปรับระบบภายในของ CherryPick เพื่อจัดการบัญชีนี้ในฐานะบอต" -flagAsCat: "เมี้ยววววววววววววววว!!!!!!!!!!!" -flagAsCatDescription: "เหมียวเหมียวเมี้ยว??" -flagShowTimelineReplies: "แสดงตอบกลับโน้ตลงไทม์ไลน์" -flagShowTimelineRepliesDescription: "เมื่อเปิดใช้งาน จะแสดงการตอบกลับของผู้ใช้คนนั้นต่อโน้ตอื่นๆ ในไทม์ไลน์ด้วย" -autoAcceptFollowed: "อนุมัติคำขอติดตามจากผู้ใช้ที่คุณติดตามอยู่โดยอัตโนมัติ" +cacheRemoteFilesDescription: "เมื่อปิดใช้งานการตั้งค่านี้ ไฟล์ระยะไกลนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกล แต่กรณีการปิดใช้งานนี้จะช่วยลดปริมาณการใช้พื้นที่จัดเก็บข้อมูล แต่เพิ่มปริมาณการใช้งาน เพราะเนื่องจากจะไม่มีการสร้างภาพขนาดย่อ" +youCanCleanRemoteFilesCache: "คุณสามารถล้างแคชได้โดยคลิกที่ปุ่ม 🗑️ ในมุมมองการจัดการไฟล์" +cacheRemoteSensitiveFiles: "ไฟล์ระยะไกลที่มีความละเอียดอ่อนแคช" +cacheRemoteSensitiveFilesDescription: "เมื่อปิดการใช้งานแล้วการตั้งค่านี้ ไฟล์รีโมตที่มีความละเอียดอ่อนนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกลโดยที่ไม่มีการแคช" +flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอท" +flagAsBotDescription: "การเปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยนักเขียนโปรแกรม หรือ ถ้าหากเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นๆ และเพื่อป้องกันการโต้ตอบแบบไม่มีที่สิ้นสุดกับบอทตัวอื่นๆ และยังสามารถปรับเปลี่ยนระบบภายในของ CherryPick เพื่อปฏิบัติต่อบัญชีนี้เป็นบอท" +flagAsCat: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว" +flagAsCatDescription: "การเปิดใช้งานตัวเลือกนี้เพื่อทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว" +flagShowTimelineReplies: "แสดงตอบกลับ ในไทม์ไลน์" +flagShowTimelineRepliesDescription: "แสดงการตอบกลับของผู้ใช้งานไปยังโน้ตของผู้ใช้งานรายอื่นๆในไทม์ไลน์หากได้เปิดเอาไว้" +autoAcceptFollowed: "อนุมัติคำขอติดตามโดยอัตโนมัติทันที จากผู้ใช้งานที่คุณกำลังติดตาม" addAccount: "เพิ่มบัญชี" reloadAccountsList: "รีโหลดรายการบัญชีใหม่" loginFailed: "การเข้าสู่ระบบไม่สำเร็จ" -showOnRemote: "ดูบนเซิร์ฟเวอร์ฝั่งระยะไกล" -continueOnRemote: "ดำเนินการต่อบนเซิร์ฟเวอร์ฝั่งระยะไกล" -chooseServerOnMisskeyHub: "เลือกเซิร์ฟเวอร์จาก Misskey Hub" -specifyServerHost: "ระบุโดเมนของเซิร์ฟเวอร์โดยตรง" -inputHostName: "โปรดป้อนโดเมน" +showOnRemote: "ดูบนอินสแตนซ์ระยะไกล" general: "ทั่วไป" -wallpaper: "ภาพพื้นหลัง" -setWallpaper: "ตั้งค่าภาพพื้นหลัง" -removeWallpaper: "นำภาพพื้นหลังออก" +wallpaper: "วอลล์เปเปอร์" +setWallpaper: "ตั้งวอลเปเปอร์" +removeWallpaper: "นำวอลเปเปอร์ออก" searchWith: "ค้นหา: {q}" -youHaveNoLists: "คุณไม่มีรายชื่อใดๆ " -followConfirm: "ต้องการติดตาม {name} ใช่ไหม?" -proxyAccount: "บัญชีพร็อกซี่" -proxyAccountDescription: "บัญชีพร็อกซี คือ บัญชีที่ทำหน้าที่ติดตาม(ผู้ใช้)ระยะไกลภายใต้เงื่อนไขบางประการ ตัวอย่างเช่น เมื่อผู้ใช้ท้องถิ่นเพิ่มผู้ใช้ระยะไกลลงรายชื่อ หากไม่มีใครติดตามผู้ใช้ระยะไกลในรายชื่อนั้น กิจกรรมก็จะไม่ถูกส่งมายังเซิร์ฟเวอร์ ดังนั้นจึงมีบัญชีพร็อกซีไว้ติดตามผู้ใช้ระยะไกลเหล่านั้น" +youHaveNoLists: "คุณไม่มีลิสต์ใด ๆ " +followConfirm: "คุณแน่ใจแล้วหรอว่าต้องการที่จะติดตาม {name}?" +proxyAccount: "บัญชี พร็อกซี่" +proxyAccountDescription: "บัญชีพร็อกซี่ คือ บัญชีที่จะทำหน้าที่เป็นผู้ติดตามระยะไกลสำหรับผู้ใช้งานที่อยู่ภายใต้ด้วยเงื่อนไขบางอย่าง ยกตัวอย่าง เช่น เมื่อมีผู้ใช้งานนั้นได้เพิ่มผู้ใช้งานจากระยะไกลลงในรายการ แต่กิจกรรมของผู้ใช้ในระยะไกลนั้นจะไม่ถูกส่งไปยังอินสแตนซ์หากไม่มีผู้ใช้งานในพื้นที่ติดตามผู้ใช้รายนั้น ดังนั้นบัญชีพร็อกซีนี้จะติดตามแทน" host: "โฮสต์" -selectSelf: "เลือกตัวเอง" selectUser: "เลือกผู้ใช้งาน" recipient: "ผู้รับ" -annotation: "หมายเหตุประกอบ" -federation: "สหพันธ์" -instances: "เซิร์ฟเวอร์" -registeredAt: "วันที่ลงทะเบียน" -latestRequestReceivedAt: "คำขอล่าสุดที่ได้รับ" +annotation: "ความคิดเห็น" +federation: "เฟดิเวิร์ส" +instances: "Server" +registeredAt: "จดทะเบียนที่" +latestRequestReceivedAt: "ได้รับคำขอล่าสุดไปแล้ว" latestStatus: "สถานะล่าสุด" storageUsage: "พื้นที่จัดเก็บข้อมูลที่ใช้ไป" -charts: "แผนภูมิ" -perHour: "ต่อชั่วโมง" +charts: "โดดเด่น" +perHour: "ทุกชั่วโมง" perDay: "ต่อวัน" stopActivityDelivery: "หยุดส่งกิจกรรม" -blockThisInstance: "บล็อกเซิร์ฟเวอร์นี้" -silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี้" -mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้" +blockThisInstance: "บล็อกอินสแตนซ์นี้" +silenceThisInstance: "ปกปิดอินสแตนซ์นี้" operations: "ดำเนินการ" software: "ซอฟต์แวร์" version: "เวอร์ชั่น" metadata: "Metadata" -withNFiles: "{n} ไฟล์" +withNFiles: "{n} ไฟล์(s)" monitor: "มอนิเตอร์" jobQueue: "คิวงาน" cpuAndMemory: "ซีพียู และ หน่วยความจำ" -network: "เครือข่าย" +network: "เน็ตเวิร์ก" disk: "ดิสก์" -instanceInfo: "ข้อมูลเซิร์ฟเวอร์" +instanceInfo: "ข้อมูล อินสแตนซ์" statistics: "สถิติการใช้งาน" clearQueue: "ล้างคิว" -clearQueueConfirmTitle: "ต้องการล้างคิวใช่ไหม?" -clearQueueConfirmText: "โพสต์ที่ยังค้างในคิวจะไม่ถูกจัดส่งอีกต่อไป โดยปกติแล้วการดำเนินการนี้ไม่จำเป็น" +clearQueueConfirmTitle: "คุณแน่ใจแล้วหรอว่าต้องการที่จะล้างคิว?" +clearQueueConfirmText: "บันทึกย่อที่ยังไม่ได้ส่งที่เหลืออยู่ในคิวนั้นมักจะ ไม่ถูกรวมเข้าด้วยกัน โดยปกติแล้วไม่จำเป็นต้องดำเนินการนี้" clearCachedFiles: "ล้างแคช" -clearCachedFilesConfirm: "ต้องการลบไฟล์ระยะไกลที่แคชไว้ทั้งหมดใช่ไหม?" -blockedInstances: "เซิร์ฟเวอร์ที่ถูกบล็อก" -blockedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการบล็อก คั่นด้วยการขึ้นบรรทัดใหม่ เซิร์ฟเวอร์ที่ถูกบล็อกจะไม่สามารถติดต่อกับอินสแตนซ์นี้ได้" -silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้แล้ว" -silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" -mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ" -mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" +clearCachedFilesConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะลบไฟล์ระยะไกลที่แคชไว้ทั้งหมด?" +blockedInstances: "อินสแตนซ์ที่ ถูกบล็อก" +blockedInstancesDescription: "ระบุชื่อโฮสต์ของอินสแตนซ์ที่คุณต้องการบล็อก อินสแตนซ์ที่อยู่ในรายการนั้นจะไม่สามารถพูดคุยกับอินสแตนซ์นี้ได้อีกต่อไป" +silencedInstances: "ปกปิดอินสแตนซ์นี้แล้ว" muteAndBlock: "ปิดเสียงและบล็อก" mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง" blockedUsers: "ผู้ใช้ที่ถูกบล็อก" noUsers: "ไม่พบผู้ใช้งาน" editProfile: "แก้ไขโปรไฟล์" -noteDeleteConfirm: "ต้องการลบโน้ตนี้ใช่ไหม?" +noteDeleteConfirm: "นายแน่ใจแล้วหรอว่าต้องการลบโน้ตนี้นะ?" pinLimitExceeded: "คุณไม่สามารถปักหมุดโน้ตเพิ่มเติมใดๆได้อีก" intro: "การติดตั้ง CherryPick เสร็จสิ้นแล้วนะ! โปรดสร้างผู้ใช้งานที่เป็นผู้ดูแลระบบ" done: "เสร็จสิ้น" @@ -249,192 +228,183 @@ processing: "กำลังประมวลผล..." preview: "แสดงตัวอย่าง" default: "ค่าเริ่มต้น" defaultValueIs: "ค่าเริ่มต้น: {value}" -noCustomEmojis: "ไม่มีเอโมจิ" -noJobs: "ไม่มีงาน" +noCustomEmojis: "ไม่มีอีโมจิ" +noJobs: "ไม่มีชิ้นงาน" federating: "สหพันธ์" blocked: "ถูกบล็อก" -suspended: "ระงับการส่ง" +suspended: "ถูกระงับ" all: "ทั้งหมด" -subscribing: "กำลังสมัครสมาชิก" +subscribing: "สมัครแล้ว" publishing: "กำลังเผยแพร่" notResponding: "ไม่มีการตอบสนอง" -instanceFollowing: "กำลังติดตามบนเซิร์ฟเวอร์" -instanceFollowers: "ผู้ติดตามของเซิร์ฟเวอร์" -instanceUsers: "ผู้ใช้ของเซิร์ฟเวอร์นี้" +instanceFollowing: "กำลังติดตาม บน อินสแตนซ์" +instanceFollowers: "ผู้ติดตามของอินสแตนซ์" +instanceUsers: "ผู้ใช้งานของอินสแตนซ์นี้" changePassword: "เปลี่ยนรหัสผ่าน" security: "ความปลอดภัย" -retypedNotMatch: "ทั้งสองป้อนข้อมูลไม่สอดคล้องกัน" +retypedNotMatch: "อินพุตไม่ตรงกันนะ" currentPassword: "รหัสผ่านปัจจุบัน" newPassword: "รหัสผ่านใหม่" newPasswordRetype: "ใส่รหัสผ่านใหม่อีกครั้ง" attachFile: "แนบไฟล์" -more: "เพิ่มเติม!" +more: "เพิ่มเติม" featured: "ไฮไลท์" usernameOrUserId: "ชื่อผู้ใช้หรือรหัสผู้ใช้งาน" noSuchUser: "ไม่พบผู้ใช้" lookup: "การค้นหา" announcements: "ประกาศ" -imageUrl: "URL รูปภาพ" +imageUrl: "url รูปภาพ" remove: "ลบ" removed: "ถูกลบไปแล้ว" -removeAreYouSure: "ต้องการลบ “{x}” ใช่ไหม?" -deleteAreYouSure: "ต้องการลบ “{x}” ใช่ไหม?" -resetAreYouSure: "รีเซ็ตเลยไหม?" -areYouSure: "แน่ใจแล้วใช่ไหมคะ?" +removeAreYouSure: "นายแน่ใจจริงหรอว่าต้องการที่จะลบออก \"{x}\"" +deleteAreYouSure: "ต้องการลบ {x} หรือไม่คะ?" +resetAreYouSure: "รีเซ็ตเลยไหม" saved: "บันทึกแล้ว" messaging: "แชท" -upload: "อัปโหลด" +upload: "อัพโหลด" keepOriginalUploading: "เก็บภาพต้นฉบับ" -keepOriginalUploadingDescription: "เก็บภาพต้นฉบับไว้เมื่ออัปโหลดภาพ หากปิด รูปภาพสำหรับการเผยแพร่ทางเว็บจะถูกสร้างขึ้นในเบราว์เซอร์เมื่อทำการอัปโหลด" +keepOriginalUploadingDescription: "บันทึกรูปภาพที่อัพโหลดต้นฉบับตามที่เป็นอยู่ ถ้าหากปิดอยู่ ระบบจะสร้างเวอร์ชั่นที่จะแสดงบนเว็บเมื่ออัพโหลดนะ" fromDrive: "จากไดรฟ์" fromUrl: "จาก URL" -uploadFromUrl: "อัปโหลดจาก URL" +uploadFromUrl: "อัพโหลดจาก URL" uploadFromUrlDescription: "URL ของไฟล์ที่คุณต้องการอัปโหลด" -uploadFromUrlRequested: "ร้องขอการอัปโหลดแล้ว" -uploadFromUrlMayTakeTime: "การอัปโหลดอาจใช้เวลาสักครู่จึงจะเสร็จสมบูรณ์" +uploadFromUrlRequested: "อัพโหลดที่ร้องขอ" +uploadFromUrlMayTakeTime: "มันอาจจะต้องใช้เวลาสักครู่จนกว่าการอัพโหลดจะเสร็จสมบูรณ์นะ" explore: "สำรวจ" messageRead: "อ่านแล้ว" -noMoreHistory: "ไม่มีประวัติเพิ่มเติม" +noMoreHistory: "ในนั้นไม่มีประวัติอีกต่อไปแล้วนะ" startMessaging: "เริ่มการสนทนา" nUsersRead: "อ่านโดย {n}" -agreeTo: "ฉันยอมรับ {0}" +agreeTo: "ฉันยอมรับที่จะ {0}" agree: "ยอมรับ" -agreeBelow: "ยอมรับตามที่ระบุด้านล่าง" +agreeBelow: "ฉันยอมรับถึงด้านล่าง" basicNotesBeforeCreateAccount: "หมายเหตุสำคัญ" termsOfService: "เงื่อนไขการให้บริการ" -start: "เริ่ม" -home: "หน้าหลัก" -remoteUserCaution: "ข้อมูลอาจไม่สมบูรณ์เนื่องจากผู้ใช้รายนี้มาจากเซิร์ฟเวอร์ระยะไกล" +start: "เริ่มต้น​ใช้งาน​" +home: "หน้าแรก" +remoteUserCaution: "เนื่องจากผู้ใช้งานรายนี้นั้น มาจากอินสแตนซ์ระยะไกล ข้อมูลที่แสดงดังกล่าวนั้นอาจจะไม่สมบูรณ์ก็ได้นะ" activity: "กิจกรรม" images: "รูปภาพ" image: "รูปภาพ" birthday: "วันเกิด" yearsOld: "{age} ปี" -registeredDate: "วันที่ลงทะเบียน" +registeredDate: "วันที่สมัครสมาชิก" location: "ตำแหน่งที่ตั้ง" theme: "ธีม" -themeForLightMode: "ธีมที่จะใช้ในโหมดสว่าง" +themeForLightMode: "ธีมที่จะใช้ในโหมดแสง" themeForDarkMode: "ธีมที่จะใช้ในโหมดมืด" light: "สว่าง" dark: "มืด" lightThemes: "ธีมสว่าง" darkThemes: "ธีมมืด" -syncDeviceDarkMode: "ซิงค์โหมดมืดกับการตั้งค่าอุปกรณ์ของคุณ" +syncDeviceDarkMode: "ซิงค์โหมดมืดด้วยการตั้งค่ากับอุปกรณ์" drive: "ไดรฟ์" fileName: "ชื่อไฟล์" selectFile: "เลือกไฟล์" selectFiles: "เลือกไฟล์" selectFolder: "เลือกโฟลเดอร์" selectFolders: "เลือกโฟลเดอร์" -fileNotSelected: "ยังไม่ได้เลือกไฟล์" renameFile: "เปลี่ยนชื่อไฟล์" -folderName: "ชื่อโฟลเดอร์" +folderName: "ชื่อแฟ้ม" createFolder: "สร้างโฟลเดอร์" renameFolder: "เปลี่ยนชื่อโฟลเดอร์" deleteFolder: "ลบโฟลเดอร์" -folder: "โฟลเดอร์" addFile: "เพิ่มไฟล์" emptyDrive: "ไดรฟ์ของคุณว่างเปล่านะ" emptyFolder: "โฟลเดอร์นี้ว่างเปล่า" unableToDelete: "ไม่สามารถลบออกได้" -inputNewFileName: "ป้อนชื่อไฟล์ใหม่" +inputNewFileName: "ป้อนชื่อไฟล์ใหม่นะ" inputNewDescription: "กรุณาใส่แคปชั่นใหม่" -inputNewFolderName: "กรุณาใส่ชื่อโฟลเดอร์ใหม่" -circularReferenceFolder: "โฟลเดอร์ปลายทางคือโฟลเดอร์ย่อยของโฟลเดอร์ที่คุณกำลังย้าย" -hasChildFilesOrFolders: "เนื่องจากโฟลเดอร์นี้ไม่ว่างเปล่า จึงไม่สามารถลบ" +inputNewFolderName: "กรุณาใส่ชื่อโฟลเดอร์ใหม่นะ\n" +circularReferenceFolder: "โฟลเดอร์ปลายทาง คือ โฟลเดอร์ย่อยของโฟลเดอร์ที่คุณต้องการที่จะย้ายล่ะนะ" +hasChildFilesOrFolders: "เนื่องจากโฟลเดอร์นี้ไม่ว่างเปล่า จึงไม่สามารถลบได้นะ" copyUrl: "คัดลอก URL" rename: "เปลี่ยนชื่อ" avatar: "ไอคอน" banner: "แบนเนอร์" -displayOfSensitiveMedia: "แสดงสื่อที่มีเนื้อหาละเอียดอ่อน" -whenServerDisconnected: "เมื่อสูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์" -disconnectedFromServer: "การเชื่อมต่อเซิร์ฟเวอร์ถูกตัด" +displayOfSensitiveMedia: "แสดงผลสื่อละเอียดอ่อน" +whenServerDisconnected: "สูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์" +disconnectedFromServer: "ถูกตัดการเชื่อมต่อออกจากเซิร์ฟเวอร์" reload: "รีโหลด" -doNothing: "ช่างมัน" -reloadConfirm: "รีโหลดเลยไหม?" -watch: "เพ่งเล็ง" -unwatch: "เลิกเพ่งเล็ง" +doNothing: "เมิน" +reloadConfirm: "นายต้องการรีเฟรชไทม์ไลน์หรือป่าว?" +watch: "ดู" +unwatch: "หยุดดู" accept: "ยอมรับ" reject: "ปฏิเสธ" normal: "ปกติ" -instanceName: "ชื่อเซิร์ฟเวอร์" -instanceDescription: "คำอธิบายแนะนำเซิร์ฟเวอร์" +instanceName: "ชื่อ อินสแตนซ์" +instanceDescription: "คำอธิบายอินสแตนซ์" maintainerName: "ผู้ดูแล" -maintainerEmail: "อีเมลผู้ดูแลระบบ" -tosUrl: "URL เงื่อนไขการให้บริการ" +maintainerEmail: "อีเมล์แอดมิน" +tosUrl: "เงื่อนไขการให้บริการ URL" thisYear: "ปีนี้" thisMonth: "เดือนนี้" today: "วันนี้" dayX: "{day}" monthX: "เดือน {month}" yearX: "{year}" -pages: "หน้าเพจ" -integration: "เชื่อมโยง" +pages: "หน้า" +integration: "รวบรวม" connectService: "เชื่อมต่อ" disconnectService: "ตัดการเชื่อมต่อ" -enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ท้องถิ่น" +enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ในพื้นที่" enableGlobalTimeline: "เปิดใช้งานไทม์ไลน์ทั่วโลก" disablingTimelinesInfo: "ผู้ดูแลระบบและผู้ควบคุมจะสามารถเข้าถึงไทม์ไลน์ทั้งหมด ถึงแม้ว่าจะไม่ได้เปิดใช้งานก็ตาม" registration: "ลงทะเบียน" enableRegistration: "เปิดใช้งานการลงทะเบียนผู้ใช้ใหม่" -invite: "คำเชิญ" -driveCapacityPerLocalAccount: "ความจุของไดรฟ์ต่อผู้ใช้ท้องถิ่น" +invite: "เชิญชวน" +driveCapacityPerLocalAccount: "ความจุของไดรฟ์ต่อผู้ใช้ภายในเครื่อง" driveCapacityPerRemoteAccount: "ความจุของไดรฟ์ต่อผู้ใช้ระยะไกล" inMb: "เป็นเมกะไบต์" bannerUrl: "URL รูปภาพแบนเนอร์" backgroundImageUrl: "URL ภาพพื้นหลัง" basicInfo: "ข้อมูลเบื้องต้น" -pinnedUsers: "ผู้ใช้ที่ถูกปักหมุด" -pinnedUsersDescription: "ป้อนชื่อผู้ใช้ที่คุณต้องการปักหมุดในหน้า “ค้นพบ” ฯลฯ คั่นด้วยการขึ้นบรรทัดใหม่" -pinnedPages: "หน้าเพจที่ปักหมุด" -pinnedPagesDescription: "ป้อนเส้นทางของหน้าเพจที่คุณต้องการปักหมุดไว้ที่หน้าแรกของเซิร์ฟเวอร์นี้ คั่นด้วยการขึ้นบรรทัดใหม่" +pinnedUsers: "ผู้ใช้งานที่ได้รับการปักหมุด" +pinnedUsersDescription: "ลิสต์ชื่อผู้ใช้โดยคั่นด้วยการขึ้นบรรทัดใหม่เพื่อปักหมุดในแท็บ \"สำรวจ\"" +pinnedPages: "หน้าที่ปักหมุด" +pinnedPagesDescription: "ป้อนเส้นทางของหน้าที่คุณต้องการตรึงไว้ที่หน้าแรกของอินสแตนซ์นี้ โดยคั่นด้วยตัวแบ่งบรรทัด" pinnedClipId: "ID ของคลิปที่จะปักหมุด" pinnedNotes: "โน้ตที่ปักหมุดไว้" hcaptcha: "hCaptcha" enableHcaptcha: "เปิดใช้ hCaptcha" hcaptchaSiteKey: "คีย์ไซต์" hcaptchaSecretKey: "คีย์ลับ" -mcaptcha: "mCaptcha" -enableMcaptcha: "เปิดใช้ mCaptcha" -mcaptchaSiteKey: "คีย์ไซต์" -mcaptchaSecretKey: "คีย์ลับ" -mcaptchaInstanceUrl: "URL ของอินสแตนซ์ของ mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "เปิดใช้ reCAPTCHA" recaptchaSiteKey: "คีย์ไซต์" recaptchaSecretKey: "คีย์ลับ" -turnstile: "Turnstile" -enableTurnstile: "เปิดใช้งาน Turnstile" +turnstile: "เทิร์น'สไทล" +enableTurnstile: "เปิดใช้งาน เทิร์น'สไทล" turnstileSiteKey: "คีย์ไซต์" turnstileSecretKey: "คีย์ลับ" -avoidMultiCaptchaConfirm: "การใช้ Captcha หลายตัวอาจทำให้เกิดการรบกวนหรือข้อผิดพลาดได้ ต้องการที่จะปิดการใช้งาน Captcha ตัวอื่นเลยไหม? หากต้องการให้เปิดใช้งานต่อไป ให้กดยกเลิก" +avoidMultiCaptchaConfirm: "การใช้ระบบ Captcha หลายระบบอาจทำให้เกิดการรบกวนหรืออาจจะเกิดข้อผิดพลาดได้ หากต้องการที่จะปิดการใช้งานระบบ Captcha อื่น ๆ แนะนำให้ปิดตัวอื่นๆก่อน ถ้าหากคุณต้องการให้เปิดใช้งานต่อไป ให้ กด ยกเลิก" antennas: "เสาอากาศ" manageAntennas: "จัดการเสาอากาศ" name: "ชื่อ" antennaSource: "แหล่งเสาอากาศ" antennaKeywords: "คีย์เวิร์ดที่ควรฟัง" antennaExcludeKeywords: "คีย์เวิร์ดที่จะยกเว้น" -antennaExcludeBots: "ยกเว้นบัญชีบอต" -antennaKeywordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR" +antennaKeywordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ" notifyAntenna: "แจ้งเตือนเกี่ยวกับโน้ตใหม่" withFileAntenna: "เฉพาะโน้ตที่มีไฟล์" -enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์ของคุณ" +enableServiceworker: "เปิดใช้งาน การแจ้งเตือนแบบพุชสำหรับเบราว์เซอร์ของคุณ" antennaUsersDescription: "ระบุหนึ่งชื่อผู้ใช้ต่อบรรทัด" -caseSensitive: "อักษรพิมพ์ใหญ่-พิมพ์เล็กความหมายต่างกัน" +caseSensitive: "กรณีที่สำคัญ" withReplies: "รวมตอบกลับ" connectedTo: "บัญชีดังต่อไปนี้มีการเชื่อมต่อกัน" notesAndReplies: "โพสต์และการตอบกลับ" -withFiles: "มีไฟล์" +withFiles: "รวบรวมไฟล์" silence: "ถูกปิดปาก" -silenceConfirm: "ต้องการปิดปากผู้ใช้รายนี้ใช่ไหม?" +silenceConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะ ปิดปาก ผู้ใช้งานรายนี้?" unsilence: "ยกเลิกการปิดปาก" -unsilenceConfirm: "ต้องการเลิกปิดปากผู้ใช้รายนี้ใช่ไหม?" +unsilenceConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะยกเลิกปิดปากผู้ใช้งานรายนี้?" popularUsers: "ผู้ใช้ที่เป็นที่นิยม" recentlyUpdatedUsers: "ผู้ใช้ที่เพิ่งใช้งานล่าสุด" recentlyRegisteredUsers: "ผู้ใช้ที่เข้าร่วมใหม่" -recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบล่าสุด" -exploreUsersCount: "มีผู้ใช้ {count} ราย" -exploreFediverse: "สำรวจสหพันธ์" +recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบใหม่" +exploreUsersCount: "มีผู้ใช้ {จำนวน} ราย" +exploreFediverse: "สำรวจเฟดดิเวิร์ส" popularTags: "แท็กยอดนิยม" userList: "ลิสต์" about: "เกี่ยวกับ" @@ -449,8 +419,8 @@ moderator: "ผู้ควบคุม" moderation: "การกลั่นกรอง" moderationNote: "โน้ตการกลั่นกรอง" addModerationNote: "เพิ่มโน้ตการกลั่นกรอง" -moderationLogs: "ปูมการควบคุมดูแล" -nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย" +moderationLogs: "บันทึกการกลั่นกรอง" +nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้" securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน" securityKey: "กุญแจความปลอดภัย" lastUsed: "ใช้ล่าสุด" @@ -459,12 +429,12 @@ unregister: "เลิกติดตาม" passwordLessLogin: "เข้าสู่ระบบแบบไม่ใช้รหัสผ่าน" passwordLessLoginDescription: "อนุญาตให้เข้าสู่ระบบโดยไม่ต้องใช้รหัสผ่านโดยใช้รหัสรักษาความปลอดภัยหรือรหัสผ่านเท่านั้น" resetPassword: "รีเซ็ตรหัสผ่าน" -newPasswordIs: "รหัสผ่านใหม่คือ “{password}”" +newPasswordIs: "รหัสผ่านใหม่คือ \"{password}\"" reduceUiAnimation: "ลดภาพเคลื่อนไหว UI" -share: "แบ่งปัน" +share: "แชร์" notFound: "ไม่พบหน้าที่ต้องการ" -notFoundDescription: "ไม่พบหน้าตาม URL ที่ระบุ" -uploadFolder: "โฟลเดอร์เริ่มต้นสำหรับอัปโหลด" +notFoundDescription: "ไม่พบหน้าที่สอดคล้องตรงกันกับ URL นี้นะ" +uploadFolder: "โฟลเดอร์เริ่มต้นสำหรับอัพโหลด" markAsReadAllNotifications: "ทำเครื่องหมายการแจ้งเตือนทั้งหมดว่าอ่านแล้ว" markAsReadAllUnreadNotes: "ทำเครื่องหมายโน้ตทั้งหมดว่าอ่านแล้ว" markAsReadAllTalkMessages: "ทำเครื่องหมายข้อความทั้งหมดว่าอ่านแล้ว" @@ -476,7 +446,7 @@ groups: "กลุ่ม" createGroup: "สร้างกลุ่ม" ownedGroups: "กลุ่มที่เป็นเจ้าของ" joinedGroups: "เข้าร่วมกลุ่ม" -invites: "คำเชิญ" +invites: "เชิญชวน" groupName: "ชื่อกลุ่ม" members: "สมาชิก" transfer: "ถ่ายโอน" @@ -487,65 +457,62 @@ text: "ข้อความ" enable: "เปิดใช้งาน" next: "ถัด​ไป" retype: "พิมพ์รหัสอีกครั้ง" -noteOf: "โน้ตของ {user}" +noteOf: "โน้ต โดย {user}" inviteToGroup: "ชวนเข้ากลุ่ม" quoteAttached: "อ้างอิง" -quoteQuestion: "ต้องการที่จะแนบมันเพื่ออ้างอิงใช่ไหม?" -attachAsFileQuestion: "ข้อความในคลิปบอร์ดยาวเกินไป คุณต้องการแนบเป็นไฟล์ข้อความหรือไม่?" -noMessagesYet: "ยังไม่มีข้อความ" +quoteQuestion: "นายต้องการที่จะอ้างอิงหรอ?" +noMessagesYet: "ยังไม่มีข้อความนะ" newMessageExists: "คุณมีข้อความใหม่" -onlyOneFileCanBeAttached: "สามารถแนบไฟล์ได้เพียงไฟล์เดียวต่อ 1 ข้อความ" -signinRequired: "ก่อนดำเนินการต่อ กรุณาลงทะเบียนหรือเข้าสู่ระบบ" -signinOrContinueOnRemote: "เพื่อดำเนินการต่อได้ คุณต้องไปที่เซิร์ฟเวอร์ที่คุณใช้งานอยู่ หรือลงทะเบียน/เข้าสู่ระบบเซิร์ฟเวอร์นี้" -invitations: "คำเชิญ" -invitationCode: "รหัสเชิญ" +onlyOneFileCanBeAttached: "คุณสามารถแนบไฟล์กับข้อความได้เพียงไฟล์เดียวเท่านั้นนะ" +signinRequired: "กรุณาลงทะเบียนหรือลงชื่อเข้าใช้ก่อนดำเนินการต่อนะ" +invitations: "เชิญชวน" +invitationCode: "รหัสคำเชิญ" checking: "Checking" available: "พร้อมใช้งาน" unavailable: "ไม่พร้อมใช้" -usernameInvalidFormat: "สามารถใช้ a~z A~Z 0~9 และ _ ได้" +usernameInvalidFormat: "คุณสามารถใช้อักษรตัวพิมพ์ใหญ่และตัวพิมพ์เล็ก ตัวเลข และขีดล่างได้นะ ( a-z , A-Z , 0-9 , รวมไปถึงอักษรพิเศษเช่น + * / , . - อื่นๆเป็นต้น )" tooShort: "สั้นเกินไปนะ" tooLong: "ยาวเกินไปนะ" -weakPassword: "รหัสผ่านแย่มาก" +weakPassword: "รหัสผ่าน แย่มาก" normalPassword: "รหัสผ่านปกติ" strongPassword: "รหัสผ่านรัดกุมมาก" passwordMatched: "ถูกต้อง!" passwordNotMatched: "ไม่ถูกต้อง" -signinWith: "เข้าสู่ระบบด้วย {x}" -signinFailed: "ไม่สามารถเข้าสู่ระบบได้ กรุณาตรวจสอบชื่อผู้ใช้และรหัสผ่าน" +signinWith: "ลงชื่อเข้าใช้ด้วย {x}" +signinFailed: "ไม่สามารถลงชื่อผู้เข้าใช้ได้ เนื่องจาก ชื่อผู้ใช้หรือรหัสผ่านที่คุณป้อนนั้นไม่ถูกต้องนะ" or: "หรือ" language: "ภาษา" uiLanguage: "ภาษาอินเทอร์เฟซผู้ใช้งาน" groupInvited: "คุณได้รับเชิญให้เข้าร่วมกลุ่ม" aboutX: "เกี่ยวกับ {x}" -emojiStyle: "สไตล์ของเอโมจิ" +emojiStyle: "สไตล์อิโมจิ" native: "ภาษาแม่" -disableDrawer: "ไม่แสดงเมนูในรูปแบบลิ้นชัก" +disableDrawer: "อย่าใช้ลิ้นชักสไตล์เมนู" youHaveNoGroups: "คุณยังไม่มีกลุ่ม" joinOrCreateGroup: "รับเชิญเข้าร่วมกลุ่มหรือสร้างกลุ่มของคุณเองเลยนะ" -showNoteActionsOnlyHover: "แสดงการดำเนินการโน้ตเมื่อโฮเวอร์(วางเมาส์เหนือ)เท่านั้น" -showReactionsCount: "แสดงจำนวนรีแอกชั่นในโน้ต" -noHistory: "ไม่มีประวัติ" +showNoteActionsOnlyHover: "แสดงการดำเนินการเฉพาะโน้ตเมื่อโฮเวอร์" +noHistory: "ไม่มีรายการ" signinHistory: "ประวัติการเข้าสู่ระบบ" enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง" -enableAnimatedMfm: "เปิดการใช้งาน MFM แบบเคลื่อนไหว" +enableAnimatedMfm: "เปิดการใช้งาน MFM ด้วยแอนิเมชั่น" doing: "กำลังประมวลผล......" category: "หมวดหมู่" tags: "นามแฝง" docSource: "ที่มาของเอกสารนี้" createAccount: "สร้างบัญชี" -existingAccount: "บัญชีที่มีอยู่แล้ว" +existingAccount: "บัญชีที่มีอยู่" regenerate: "สร้างอีกครั้ง" fontSize: "ขนาดตัวอักษร" -mediaListWithOneImageAppearance: "ความสูงของรายการสื่อที่มีเพียงรูปเดียว" +mediaListWithOneImageAppearance: "ความสูงของลิสต์สื่อจะต้องมีรูปภาพเดียวเท่านั้น" limitTo: "จำกัดไว้ที่ {x}" noFollowRequests: "คุณไม่มีคำขอติดตามที่รอดำเนินการ" openImageInNewTab: "เปิดรูปภาพในแท็บใหม่" dashboard: "หน้ากระดานหลัก" -local: "ท้องถิ่น" +local: "ในพื้นที่" remote: "ระยะไกล" total: "รวมทั้งหมด" -weekOverWeekChanges: "เทียบกับสัปดาห์ก่อน" -dayOverDayChanges: "เทียบกับเมื่อวาน" +weekOverWeekChanges: "เปลี่ยนแปลงไปเมื่อสัปดาห์ที่แล้ว" +dayOverDayChanges: "เปลี่ยนแปลงไปเมื่อวานนี้" appearance: "ภาพลักษณ์" clientSettings: "การตั้งค่าไคลเอนต์" accountSettings: "ตั้งค่าบัญชี" @@ -554,29 +521,28 @@ promote: "โปรโมท" numberOfDays: "จำนวนวัน" hideThisNote: "ซ่อนโน้ตนี้" showFeaturedNotesInTimeline: "แสดงโน้ตเด่นในไทม์ไลน์" -objectStorage: "การจัดเก็บในรูปแบบอ็อบเจกต์" -useObjectStorage: "ใช้การจัดเก็บในรูปแบบอ็อบเจกต์" -objectStorageBaseUrl: "Base URL" +objectStorage: "อ็อบเจ็กต์ ที่จัดเก็บ" +useObjectStorage: "ใช้ อ็อบเจ็กต์ ที่จัดเก็บ" +objectStorageBaseUrl: "URL ฐาน" objectStorageBaseUrlDesc: "URL ที่ใช้เป็นข้อมูลอ้างอิง ระบุ URL ของ CDN หรือ Proxy ถ้าหากคุณใช้อย่างใดอย่างหนึ่ง\n สำหรับการใช้งาน S3 'https://.s3.amazonaws.com' และสำหรับ GCS หรือบริการที่เทียบเท่าใช้ 'https://storage.googleapis.com/', เป็นต้น" objectStorageBucket: "Bucket" -objectStorageBucketDesc: "โปรดระบุชื่อบัคเก็ตของบริการที่ใช้อยู่" +objectStorageBucketDesc: "โปรดระบุชื่อที่เก็บข้อมูลที่ใช้กับผู้ให้บริการของคุณ" objectStoragePrefix: "คำนำหน้า" -objectStoragePrefixDesc: "ไฟล์ทั้งหมดจะถูกเก็บไว้ภายใต้ไดเร็กทอรีที่มีคำนำหน้านี้" +objectStoragePrefixDesc: "ไฟล์ทั้งหมดจะถูกเก็บไว้ภายใต้ไดเร็กทอรีที่มีคำนำหน้านี้นะ" objectStorageEndpoint: "ปลายทาง" objectStorageEndpointDesc: "เว้นว่างไว้หากคุณใช้ AWS S3 หรือระบุปลายทางเป็น '' หรือ ':' ทั้งนี้ขึ้นอยู่กับผู้ให้บริการที่คุณใช้อยู่ด้วย" objectStorageRegion: "ภูมิภาค" -objectStorageRegionDesc: "ระบุภูมิภาค เช่น ‘xx-east-1’ หากบริการของคุณไม่แยกภูมิภาค ให้ระบุเป็น ‘us-east-1’ หรือเว้นวางไว้หากใช้ AWS configuration files / environment variables" +objectStorageRegionDesc: "ระบุภูมิภาค เช่น 'xx-east-1' ถ้าหากบริการของคุณไม่ได้แยกความแตกต่างระหว่างภูมิภาคก็ให้ เว้นว่างไว้หรือป้อน 'us-east-1'" objectStorageUseSSL: "ใช้ SSL" objectStorageUseSSLDesc: "ปิดการทำงานนี้ไว้ ถ้าหากคุณจะไม่ใช้ HTTPS สำหรับการเชื่อมต่อ API" objectStorageUseProxy: "เชื่อมต่อผ่านพร็อกซี" objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหากคุณจะไม่ใช้ Proxy สำหรับการเชื่อมต่อ API" -objectStorageSetPublicRead: "ตั้งค่าเป็น “public-read” เมื่ออัปโหลด" -s3ForcePathStyleDesc: "เมื่อเปิดใช้งาน s3ForcePathStyle จะบังคับให้ ระบุชื่อบัคเก็ตเป็นส่วนหนึ่งของพาธ แทนที่จะเป็นชื่อโฮสต์ใน URL, อาจจำเป็นต้องเปิดใช้งานตัวเลือกนี้เมื่อใช้กับ Minio ที่โฮสต์เองหรือบริการที่คล้ายกัน" -serverLogs: "ปูมของเซิร์ฟเวอร์" +objectStorageSetPublicRead: "ตั้งค่า \"public-read\" ในการอัปโหลด" +s3ForcePathStyleDesc: "ถ้าหากเปิดใช้งาน s3ForcePathStyle ชื่อบัคเก็ตนั้นอาจจะต้องรวมอยู่ในเส้นทางของ URL ซึ่งตรงข้ามกับชื่อโฮสต์ของ URL คุณอาจจะต้องเปิดใช้งานการตั้งค่านี้เมื่อใช้บริการต่างๆ เช่น อินสแตนซ์ Minio ที่โฮสต์เองนะ" +serverLogs: "บันทึกของเซิร์ฟเวอร์" deleteAll: "ลบทั้งหมด" showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์" -showFixedPostFormInChannel: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนของไทม์ไลน์ (ช่อง)" -withRepliesByDefaultForNewlyFollowed: "แสดงการตอบกลับจากผู้ใช้ที่คุณเพิ่งติดตามลงไทม์ไลน์ตามค่าเริ่มต้น" +showFixedPostFormInChannel: "แสดงแบบฟอร์มกำลังโพสต์ที่ด้านบนของไทม์ไลน์ (แชนแนล)" newNoteRecived: "มีโน้ตใหม่" sounds: "เสียง" sound: "เสียง" @@ -584,12 +550,10 @@ listen: "ฟัง" none: "ไม่มี" showInPage: "แสดงในเพจ" popout: "ป๊อปเอาต์" -volume: "ระดับเสียง" -masterVolume: "ระดับเสียงหลัก" -notUseSound: "ไม่ใช้เสียง" -useSoundOnlyWhenActive: "มีเสียงออกเฉพาะตอนกำลังใช้ CherryPick อยู่เท่านั้น" +volume: "ความดัง" +masterVolume: "มาสเตอร์วอลุ่ม" details: "รายละเอียด" -chooseEmoji: "เลือกเอโมจิ" +chooseEmoji: "เลือกโมจิของเธอ" unableToProcess: "ไม่สามารถดำเนินการให้เสร็จสิ้นได้" recentUsed: "ใช้ล่าสุด" install: "ติดตั้ง" @@ -600,37 +564,37 @@ installedDate: "วันที่ติดตั้ง" lastUsedDate: "ใช้งานครั้งล่าสุด" state: "สถานะ" sort: "เรียงลำดับ" -ascendingOrder: "เรียงลำดับขึ้น" -descendingOrder: "เรียงลำดับลง" -scratchpad: "Scratchpad" -scratchpadDescription: "Scratchpad ให้สภาพแวดล้อมสำหรับการทดลอง AiScript คุณสามารถเขียนโค้ด/สั่งดำเนินการ/ตรวจสอบผลลัพธ์ ของการโต้ตอบกับ CherryPick ได้" +ascendingOrder: "เรียงจากน้อยไปมาก" +descendingOrder: "เรียงจากมากไปน้อย" +scratchpad: "กระดานทดลอง" +scratchpadDescription: "Scratchpad เป็นการจัดเตรียมสภาพแวดล้อมสำหรับการทดลอง AiScript แต่คุณสามารถเขียน ดำเนินการ และตรวจสอบผลลัพธ์ของการโต้ตอบกับ CherryPick มันได้ด้วยนะ" output: "เอาท์พุต" script: "สคริปต์" disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ" updateRemoteUser: "อัปเดตข้อมูลผู้ใช้งานระยะไกล" unsetUserAvatar: "เลิกตั้งอวตาร" -unsetUserAvatarConfirm: "ต้องการเลิกตั้งอวตารใข่ไหม?" +unsetUserAvatarConfirm: "คุณแน่ใจหรือไม่ว่าต้องการเลิกตั้งอวตาร?" unsetUserBanner: "เลิกตั้งแบนเนอร์" -unsetUserBannerConfirm: "ต้องการเลิกตั้งแบนเนอร์?" +unsetUserBannerConfirm: "คุณแน่ใจหรือไม่ว่าต้องการเลิกตั้งแบนเนอร์เลยมั้ย?" deleteAllFiles: "ลบไฟล์ทั้งหมด" -deleteAllFilesConfirm: "ต้องการลบไฟล์ทั้งหมดใช่ไหม?" +deleteAllFilesConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะลบไฟล์ทั้งหมด?" removeAllFollowing: "เลิกติดตามผู้ใช้ที่ติดตามทั้งหมด" -removeAllFollowingDescription: "จะเลิกติดตามทั้งหมดจาก {host} โปรดดำเนินการสิ่งนี้เมื่อเซิร์ฟเวอร์ดังกล่าวได้สูญหายตายจากไปแล้ว" +removeAllFollowingDescription: "การที่คุณดำเนินการนี้จะเลิกติดตามบัญชีทั้งหมดจาก {host} โปรดเรียกใช้คำสั่งสิ่งนี้หากต้องการยกเลิกอินสแตนซ์ เช่น ไม่มีอยู่แล้ว" userSuspended: "ผู้ใช้รายนี้ถูกระงับการใช้งาน" -userSilenced: "ผู้ใช้รายนี้ถูกปิดปากอยู่" +userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น" yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ" yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่" tokenRevoked: "โทเค็นไม่ถูกต้อง" -tokenRevokedDescription: "โทเค็นการเข้าสู่ระบบหมดอายุ กรุณาเข้าสู่ระบบใหม่อีกครั้ง" +tokenRevokedDescription: "โทเค็นนี้หมดอายุแล้วนะค่ะกรุณาเข้าสู่ระบบอีกครั้งนะ" accountDeleted: "ลบบัญชีแล้ว" -accountDeletedDescription: "บัญชีนี้ถูกลบแล้ว" +accountDeletedDescription: "บัญชีนี้ถูกลบไปแล้วนะ" menu: "เมนู" divider: "ตัวแบ่ง" addItem: "เพิ่มรายการ" rearrange: "จัดใหม่" relays: "รีเลย์" addRelay: "เพิ่มรีเลย์" -inboxUrl: "URL ของอินบ็อกซ์" +inboxUrl: "อินบ็อกซ์ URL" addedRelays: "เพิ่มรีเลย์แล้ว" serviceworkerInfo: "ต้องเปิดใช้งานสำหรับการแจ้งเตือนแบบพุช" deletedNote: "โน้ตที่ถูกลบ" @@ -643,38 +607,37 @@ enablePlayer: "เปิดเครื่องเล่นวิดีโอ" disablePlayer: "ปิดเครื่องเล่นวิดีโอ" expandTweet: "ขยายทวีต" themeEditor: "ตัวแก้ไขธีม" -description: "คำอธิบาย" +description: "รายละเอียด" describeFile: "เพิ่มแคปชั่น" enterFileDescription: "ใส่แคปชั่น" author: "ผู้เขียน" -leaveConfirm: "มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก ต้องการละทิ้งมันใช่ไหม?" +leaveConfirm: "คุณมีการเปลี่ยนแปลงที่ไม่ได้บันทึกนะ นายต้องการทิ้งการเปลี่ยนแปลงเหล่านั้นหรอ?" manage: "การจัดการ" plugins: "ปลั๊กอิน" -preferencesBackups: "สำรองการตั้งค่า" +preferencesBackups: "ตั้งค่าการสำรองข้อมูล" deck: "เด็ค" undeck: "ออกจากเด็ค" useBlurEffectForModal: "ใช้เอฟเฟกต์เบลอสำหรับโมดอล" -useFullReactionPicker: "ใช้ตัวจิ้มรีแอคชั่นอย่างเต็มรูปแบบ" +useFullReactionPicker: "ใช้เครื่องมือเลือกปฏิกิริยาขนาดเต็ม" width: "ความกว้าง" height: "ความสูง" large: "ใหญ่" medium: "ปานกลาง" small: "เล็ก" -generateAccessToken: "สร้างโทเค็นการเข้าถึง" -permission: "สิทธิ์" -adminPermission: "สิทธิ์ของผู้ดูแลระบบ" +generateAccessToken: "สร้างการเข้าถึงโทเค็น" +permission: "การอนุญาต" enableAll: "เปิดใช้งานทั้งหมด" disableAll: "ปิดการใช้งานทั้งหมด" tokenRequested: "ให้สิทธิ์การเข้าถึงบัญชี" -pluginTokenRequestedDescription: "ปลั๊กอินนี้จะใช้สิทธิ์ตามที่ตั้งค่าไว้ที่นี่" +pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ" notificationType: "ประเภทการแจ้งเตือน" edit: "แก้ไข" -emailServer: "เซิร์ฟเวอร์ของอีเมล" +emailServer: "อีเมล์เซิร์ฟเวอร์" enableEmail: "เปิดใช้งานการกระจายอีเมล" -emailConfigInfo: "ใช้สำหรับการยืนยันอีเมลหรือการรีเซ็ตรหัสผ่าน" -email: "อีเมล" -emailAddress: "ที่อยู่อีเมล" -smtpConfig: "ตั้งค่าเซิร์ฟเวอร์ SMTP" +emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน" +email: "อีเมล์" +emailAddress: "ที่อยู่อีเมล์" +smtpConfig: "กำหนดค่าเซิร์ฟเวอร์ SMTP" smtpHost: "โฮสต์" smtpPort: "พอร์ต" smtpUser: "ชื่อผู้ใช้" @@ -684,52 +647,51 @@ smtpSecure: "ใช้โดยนัย SSL/TLS สำหรับการเ smtpSecureInfo: "ปิดสิ่งนี้เมื่อใช้ STARTTLS" testEmail: "ทดสอบการส่งอีเมล" wordMute: "ปิดเสียงคำ" -hardWordMute: "ปิดเสียงคำแบบแข็งโป๊ก" -regexpError: "เกิดข้อผิดพลาดใน regular expression" -regexpErrorDescription: "เกิดข้อผิดพลาดใน regular expression บรรทัดที่ {line} ของการปิดเสียงคำ {tab} :" -instanceMute: "ปิดเสียงเซิร์ฟเวอร์" +hardWordMute: "ปิดเสียงคำยาก" +regexpError: "ข้อผิดพลาดของนิพจน์ทั่วไป" +regexpErrorDescription: "เกิดข้อผิดพลาดในนิพจน์ทั่วไปในบรรทัดที่ {line} ของการปิดเสียงคำ {tab} ของคุณ:" +instanceMute: "ปิดเสียง อินสแตนซ์" userSaysSomething: "{name} พูดอะไรบางอย่าง" makeActive: "เปิดใช้งาน" display: "แสดงผล" copy: "คัดลอก" metrics: "เมตริก" overview: "ภาพรวม" -logs: "ปูม" +logs: "บันทึกข้อมูลระบบ" delayed: "ดีเลย์" database: "ฐานข้อมูล" -channel: "ช่อง" +channel: "แชนแนล" create: "สร้าง" notificationSetting: "ตั้งค่าการแจ้งเตือน" notificationSettingDesc: "เลือกประเภทการแจ้งเตือนที่ต้องการจะแสดง" useGlobalSetting: "ใช้การตั้งค่าส่วนกลาง" -useGlobalSettingDesc: "เมื่อเปิดใช้งาน ใช้การตั้งค่าการแจ้งเตือนจากบัญชีคุณ เมื่อปิดใช้งาน สามารถตั้งค่าได้อย่างอิสระ" +useGlobalSettingDesc: "หากเปิดไว้ ระบบจะใช้การตั้งค่าการแจ้งเตือนของบัญชีของคุณ หากปิดอยู่ สามารถทำการกำหนดค่าแต่ละรายการได้นะ" other: "อื่น ๆ" regenerateLoginToken: "สร้างโทเค็นการเข้าสู่ระบบอีกครั้ง" regenerateLoginTokenDescription: "สร้างโทเค็นใหม่ที่ใช้ภายในระหว่างการเข้าสู่ระบบ โดยตามหลักปกติแล้วการดำเนินการนี้ไม่จำเป็น หากสร้างใหม่ อุปกรณ์ทั้งหมดจะถูกออกจากระบบนะ" -theKeywordWhenSearchingForCustomEmoji: "คีย์เวิร์ดสำหรับใช้ค้นหาเอโมจิที่กำหนดเอง" setMultipleBySeparatingWithSpace: "คั่นหลายรายการด้วยช่องว่าง" -fileIdOrUrl: "ID ของไฟล์ หรือ URL" +fileIdOrUrl: "ไฟล์ ID หรือ URL" behavior: "พฤติกรรม" sample: "ตัวอย่าง" abuseReports: "รายงาน" reportAbuse: "รายงาน" reportAbuseRenote: "รายงานรีโน้ต" -reportAbuseOf: "รายงาน {name}" +reportAbuseOf: "รายงาน {ชื่อ}" fillAbuseReportDescription: "กรุณากรอกรายละเอียดเกี่ยวกับรายงานนี้ หากเป็นเรื่องเกี่ยวกับโน้ตโดยเฉพาะ ได้โปรดระบุ URL" abuseReported: "เราได้ส่งรายงานของคุณไปแล้ว ขอบคุณมากๆนะ" -reporter: "ผู้รายงาน" -reporteeOrigin: "ปลายทางรายงาน" -reporterOrigin: "แหล่งผู้รายงาน" -forwardReport: "ส่งต่อรายงานไปยังเซิร์ฟเวอร์ระยะไกล" -forwardReportIsAnonymous: "ข้อมูลของคุณจะไม่ปรากฏบนเซิร์ฟเวอร์ระยะไกลและปรากฏเป็นบัญชีระบบที่ไม่ระบุชื่อ" +reporter: "นักข่าว" +reporteeOrigin: "รายงานต้นทาง" +reporterOrigin: "นักข่าวต้นทาง" +forwardReport: "ส่งต่อรายงานไปยังอินสแตนซ์ระยะไกล" +forwardReportIsAnonymous: "แทนที่จะเป็นบัญชีของคุณ บัญชีระบบที่ไม่ระบุตัวตนจะแสดงเป็นนักข่าวที่อินสแตนซ์ระยะไกล" send: "ส่ง" abuseMarkAsResolved: "ทำเครื่องหมายรายงานว่าแก้ไขแล้ว" openInNewTab: "เปิดในแท็บใหม่" openInSideView: "เปิดในมุมมองด้านข้าง" defaultNavigationBehaviour: "พฤติกรรมการนำทางที่เป็นค่าเริ่มต้น" editTheseSettingsMayBreakAccount: "การแก้ไขการตั้งค่าเหล่านี้อาจทำให้บัญชีของคุณเสียหายนะ" -instanceTicker: "ข้อมูลเซิร์ฟเวอร์ของโน้ต" -waitingFor: "กำลังรอ {x}" +instanceTicker: "ข้อมูลอินสแตนซ์ของบันทึกย่อ" +waitingFor: "กำลังรอคอย {x}" random: "สุ่มค่า" system: "ระบบ" switchUi: "สลับ UI" @@ -739,7 +701,7 @@ createNew: "สร้างใหม่" optional: "ไม่บังคับ" createNewClip: "สร้างคลิปใหม่" unclip: "ลบคลิป" -confirmToUnclipAlreadyClippedNote: "โน้ตนี้เป็นส่วนหนึ่งของคลิป “{name}” อยู่แล้ว ต้องการนำมันออกจากคลิปใช่ไหม?" +confirmToUnclipAlreadyClippedNote: "โน้ตนี้เป็นส่วนหนึ่งของคลิป \"{name}\" แล้ว คุณต้องการลบออกจากคลิปนี้แทนอย่างงั้นหรอ?" public: "สาธารณะ" private: "ส่วนตัว" i18nInfo: "CherryPick กำลังได้รับการแปลเป็นภาษาต่างๆ โดยอาสาสมัคร คุณสามารถช่วยเหลือได้ที่ {link}" @@ -752,8 +714,8 @@ repliedCount: "จำนวนของการตอบกลับที่ renotedCount: "จำนวนรีโน้ตที่ได้รับแล้ว" followingCount: "จำนวนบัญชีที่ติดตาม" followersCount: "จำนวนผู้ติดตาม" -sentReactionsCount: "จำนวนรีแอคชั่นที่ส่ง" -receivedReactionsCount: "จำนวนรีแอคชั่นที่ได้รับ" +sentReactionsCount: "จำนวนปฏิกิริยาที่ส่ง" +receivedReactionsCount: "จำนวนปฏิกิริยาที่ได้รับ" pollVotesCount: "จำนวนโหวตที่ส่งไป" pollVotedCount: "จำนวนโหวตที่ได้รับ" yes: "ใช่" @@ -761,108 +723,107 @@ no: "ไม่" driveFilesCount: "จำนวนไฟล์ไดรฟ์" driveUsage: "การใช้พื้นที่ไดรฟ์" noCrawle: "ปฏิเสธการจัดทำดัชนีของโปรแกรมรวบรวมข้อมูล" -noCrawleDescription: "ขอให้เครื่องมือค้นหาไม่จัดทำดัชนีหน้าโปรไฟล์ โน้ต หน้าเพจ ฯลฯ" -lockedAccountInfo: "แม้ว่าการอนุมัติการติดตามถูกเปิดใช้งานอยู่ทุกคนก็ยังคงสามารถเห็นโน้ตของคุณได้ เว้นแต่ว่าคุณจะเปลี่ยนการเปิดเผยโน้ตของคุณเป็น “เฉพาะผู้ติดตาม”" -alwaysMarkSensitive: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อนเป็นค่าเริ่มต้น" +noCrawleDescription: "ขอให้เครื่องมือค้นหาไม่จัดทำดัชนีหน้าโปรไฟล์ บันทึกย่อ หน้า ฯลฯ" +lockedAccountInfo: "เว้นแต่ว่าคุณจะต้องตั้งค่าการเปิดเผยโน้ตเป็น \"ผู้ติดตามเท่านั้น\" โน้ตย่อของคุณจะปรากฏแก่ทุกคน ถึงแม้ว่าคุณจะเป็นกำหนดให้ผู้ติดตามต้องได้รับการอนุมัติด้วยตนเองก็ตาม" +alwaysMarkSensitive: "ทำเครื่องหมายเป็น NSFW เป็นค่าเริ่มต้น" loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ" disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว" -highlightSensitiveMedia: "ไฮไลท์สื่อที่มีเนื้อหาละเอียดอ่อน" -verificationEmailSent: "ได้ส่งอีเมลยืนยันแล้ว กรุณาเข้าลิงก์ที่ระบุในอีเมลเพื่อทำการตั้งค่าให้เสร็จสิ้น" +highlightSensitiveMedia: "ไฮไลท์สื่อที่ละเอียดอ่อน" +verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น" notSet: "ไม่ได้ตั้งค่า" emailVerified: "อีเมลได้รับการยืนยันแล้ว" noteFavoritesCount: "จำนวนโน้ตที่ชื่นชอบ" -pageLikesCount: "จำนวนเพจที่ถูกใจ" +pageLikesCount: "จำนวนเพจที่ชอบ" pageLikedCount: "จำนวนการกดถูกใจเพจที่ได้รับแล้ว" contact: "ติดต่อ" useSystemFont: "ใช้ฟอนต์เริ่มต้นของระบบ" clips: "คลิป" experimentalFeatures: "ฟังก์ชั่นทดสอบ" experimental: "ทดลอง" -thisIsExperimentalFeature: "นี่เป็นฟีเจอร์ทดลอง ซึ่งอาจมีการเปลี่ยนแปลงการทำงาน และอาจไม่ทำงานตามที่ตั้งใจไว้" +thisIsExperimentalFeature: "นี่คือฟีเจอร์ทดลองนะค่ะ ฟังก์ชันการทำงานบางอย่างอาจเปลี่ยนแปลงได้ และอาจไม่ทำงานหรือไม่เสถียรตามที่ตั้งใจไว้นะ" developer: "สำหรับนักพัฒนา" -makeExplorable: "ทำให้บัญชีมองเห็นใน “สำรวจ”" -makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน “สำรวจ”" +makeExplorable: "ทำให้บัญชีมองเห็นใน \"สำรวจ\"" +makeExplorableDescription: "ถ้าหากคุณปิดการทำงานนี้ บัญชีของคุณนั้นจะไม่แสดงในส่วน \"สำรวจ\" นะ" showGapBetweenNotesInTimeline: "แสดงช่องว่างระหว่างโพสต์บนไทม์ไลน์" duplicate: "ทำซ้ำ" left: "ซ้าย" -center: "กึ่งกลาง" +center: "ศูนย์กลาง" wide: "กว้าง" narrow: "ชิด" -reloadToApplySetting: "การตั้งค่านี้จะมีผลหลังจากโหลดหน้าซ้ำเท่านั้น ต้องการที่จะโหลดใหม่เลยไหม?" -needReloadToApply: "ต้องรีโหลดเพื่อให้การเปลี่ยนแปลงมีผล" +reloadToApplySetting: "การตั้งค่านี้จะมีผลหลังจากโหลดหน้าซ้ำเท่านั้น ต้องการที่จะโหลดใหม่เลยมั้ย" +needReloadToApply: "จำเป็นต้องโหลดซ้ำถึงจะมีผลนะ" showTitlebar: "แสดงแถบชื่อ" clearCache: "ล้างแคช" -onlineUsersCount: "{n} รายกำลังออนไลน์" +onlineUsersCount: "{n} ผู้ใช้คนนี้กำลังออนไลน์" nUsers: "{n} ผู้ใช้งาน" nNotes: "{n} โน้ต" -sendErrorReports: "ส่งรายงานข้อผิดพลาด" -sendErrorReportsDescription: "เมื่อเปิดใช้งาน การแจ้งข้อผิดพลาดจะถูกแชร์กับ CherryPick เมื่อเกิดปัญหา ซึ่งช่วยในการปรับปรุงคุณภาพของซอฟต์แวร์ ข้อมูลข้อผิดพลาดอาจรวมถึงเวอร์ชันของระบบปฏิบัติการ ประเภทของเบราว์เซอร์ และประวัติการใช้งาน ฯลฯ" +sendErrorReports: "ส่งรายงานว่าข้อผิดพลาด" +sendErrorReportsDescription: "เมื่อเปิดใช้งาน ข้อมูลข้อผิดพลาดโดยรายละเอียดนั้นจะถูกแชร์ให้กับ CherryPick เมื่อเกิดปัญหา ซึ่งช่วยปรับปรุงคุณภาพของ CherryPick\nซึ่งจะรวมถึงข้อมูล เช่น เวอร์ชั่นของระบบปฏิบัติการ เบราว์เซอร์ที่คุณใช้ กิจกรรมของคุณใน CherryPick เป็นต้น" myTheme: "ธีมของฉัน" -backgroundColor: "สีพื้นหลัง" -accentColor: "สีหลัก" +backgroundColor: "ภาพพื้นหลัง" +accentColor: "รูปแบบสี" textColor: "สีข้อความ" saveAs: "บันทึกเป็น..." advanced: "ขั้นสูง" advancedSettings: "การตั้งค่าขั้นสูง" value: "ค่า" createdAt: "สร้างเมื่อ" -updatedAt: "อัปเดตล่าสุด" +updatedAt: "อัพเดทล่าสุด" saveConfirm: "บันทึกเปลี่ยนแปลงมั้ย?" -deleteConfirm: "ต้องการลบใช่ไหม?" +deleteConfirm: "ลบจริงๆเหรอ?" invalidValue: "ค่านี้ไม่ถูกต้อง" registry: "ทะเบียน" closeAccount: "ปิด บัญชี" currentVersion: "เวอร์ชั่นปัจจุบัน" -latestVersion: "เวอร์ชั่นล่าสุด" +latestVersion: "รุ่นปัจจุบัน" youAreRunningUpToDateClient: "คุณกำลังใช้ไคลเอ็นต์เวอร์ชันใหม่ล่าสุดนะ" newVersionOfClientAvailable: "มีไคลเอ็นต์เวอร์ชันใหม่กว่าของคุณพร้อมใช้งานนะ" usageAmount: "การใช้งาน" capacity: "ความจุ" inUse: "ใช้แล้ว" editCode: "แก้ไขโค้ด" -apply: "นำไปใช้" -receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากเซิร์ฟเวอร์นี้" -emailNotification: "การแจ้งเตือนทางอีเมล" +apply: "ตกลง" +receiveAnnouncementFromInstance: "รับการแจ้งเตือนจากอินสแตนซ์นี้" +emailNotification: "การแจ้งเตือนทางอีเมล์" publish: "เผยแพร่" inChannelSearch: "ค้นหาในช่อง" -useReactionPickerForContextMenu: "คลิกขวาเพื่อเปิดตัวจิ้มรีแอคชั่น" -typingUsers: "{users} กำลังพิมพ์..." +useReactionPickerForContextMenu: "เปิดตัวเลือกปฏิกิริยาเมื่อคลิกขวา" +typingUsers: "{users} กำลัง/กำลังพิมพ์..." jumpToSpecifiedDate: "ข้ามไปยังวันที่เฉพาะเจาะจง" showingPastTimeline: "กำลังแสดงผลไทม์ไลน์เก่า" clear: "ล้าง" markAllAsRead: "ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว" goBack: "ย้อนกลับ" -unlikeConfirm: "ต้องการเลิกถูกใจใช่ไหม?" +unlikeConfirm: "ลบไลค์ของคุณออกจริงๆหรอ" fullView: "มุมมองแบบเต็ม" quitFullView: "ออกจากมุมมองแบบเต็ม" addDescription: "เพิ่มคำอธิบาย" -userPagePinTip: "ปักหมุดโน้ตให้แสดงที่นี่ได้โดยเลือกเมนู “ปักหมุด” ของโน้ตนั้นๆ" +userPagePinTip: "คุณสามารถแสดงผลโน้ตย่อได้ที่นี่โดยเลือก \"ปักหมุดที่โปรไฟล์\" จากเมนูของโน้ตย่อแต่ละรายการนะ" notSpecifiedMentionWarning: "โน้ตนี้มีการกล่าวถึงผู้ใช้งานที่ไม่รวมอยู่ในผู้รับ" info: "เกี่ยวกับ" userInfo: "ข้อมูลผู้ใช้" unknown: "ไม่ทราบสถานะ" onlineStatus: "สถานะออนไลน์" hideOnlineStatus: "ซ่อนสถานะออนไลน์" -hideOnlineStatusDescription: "การซ่อนสถานะออนไลน์อาจทำให้ฟังก์ชันบางอย่าง เช่น การค้นหา สะดวกน้อยลง" +hideOnlineStatusDescription: "การซ่อนสถานะออนไลน์ของคุณช่วยลดความสะดวกของคุณสมบัติบางอย่าง เช่น การค้นหา อ่ะนะ" online: "ออนไลน์" active: "ใช้งานอยู่" offline: "ออฟไลน์" -notRecommended: "ไม่แนะนำ" -botProtection: "การป้องกัน Bot" -instanceBlocking: "เซิร์ฟเวอร์ที่ถูกบล็อก/ปิดปาก" +notRecommended: "ไม่ใช้งาน" +botProtection: "การป้องกัน Bot (or AI)" +instanceBlocking: "อินสแตนซ์ที่ถูกบล็อก" selectAccount: "เลือกบัญชี" switchAccount: "สลับบัญชีผู้ใช้" enabled: "เปิดใช้งาน" disabled: "ปิดการใช้งาน" quickAction: "ปุ่มลัด" -user: "ผู้ใช้" +user: "ผู้ใช้งาน" administration: "การจัดการ" accounts: "บัญชีผู้ใช้" switch: "สลับ" -noMaintainerInformationWarning: "ยังไม่ได้ตั้งค่าข้อมูลของผู้ดูแลระบบ" -noInquiryUrlWarning: "ยังไม่ได้ตั้งค่า URL สำหรับการติดต่อสอบถาม" -noBotProtectionWarning: "ยังไม่ได้ตั้งค่าการป้องกันบอต" -configure: "ตั้งค่า" +noMaintainerInformationWarning: "ข้อมูลผู้ดูแลไม่ได้รับการกำหนดค่านะ" +noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ" +configure: "กำหนดค่า" postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่" postToHashtag: "โพสต์ไปที่แฮชแท็กนี้" gallery: "แกลเลอรี่" @@ -871,19 +832,19 @@ popularPosts: "โพสต์ติดอันดับ" shareWithNote: "แบ่งปันด้วยโน้ต" ads: "โฆษณา" expiration: "กำหนดเวลา" -startingperiod: "เริ่มเมื่อ" -memo: "เมโม" +startingperiod: "เริ่ม" +memo: "ข้อควรจำ" priority: "ลำดับความสำคัญ" high: "สูง" middle: "ปานกลาง" low: "ต่ำ" -emailNotConfiguredWarning: "ยังไม่ได้ตั้งค่าที่อยู่อีเมล" +emailNotConfiguredWarning: "ไม่ได้ตั้งค่าที่อยู่อีเมลนะ" ratio: "อัตราส่วน" previewNoteText: "แสดงตัวอย่าง" customCss: "CSS ที่กำหนดเอง" -customCssWarn: "ควรใช้การตั้งค่านี้เฉพาะต่อเมื่อคุณรู้มันใช้ทำอะไร การตั้งค่าที่ไม่เหมาะสมอาจทำให้ไคลเอ็นต์ไม่สามารถใช้งานได้อย่างถูกต้อง" +customCssWarn: "ควรใช้การตั้งค่านี้เฉพาะต่อเมื่อคุณรู้ว่าการตั้งค่านี้ใช้ทำอะไร การป้อนค่าที่ไม่เหมาะสมอาจทำให้ไคลเอ็นต์หยุดทำงานตามปกติได้นะ" global: "ทั่วโลก" -squareAvatars: "แสดงผลอวตารเป็นสี่เหลี่ยม" +squareAvatars: "แสดงผลอวตารสี่เหลี่ยม" sent: "ส่ง" received: "ได้รับแล้ว" searchResult: "ผลการค้นหา" @@ -900,10 +861,10 @@ usernameInfo: "ชื่อที่ระบุบัญชีของคุ aiChanMode: "โหมด Ai " devMode: "โหมดนักพัฒนา" keepCw: "เก็บคำเตือนเนื้อหา" -pubSub: "บัญชี Pub/Sub" +pubSub: "บัญชีผับ/ย่อย" lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด" resolved: "คลี่คลายแล้ว" -unresolved: "ยังไม่ได้รับการแก้ไข" +unresolved: "รอการเฉลย" breakFollow: "ลบผู้ติดตาม" breakFollowConfirm: "ลบผู้ติดตามนี้ออกจริงหรอ?" itsOn: "เปิดใช้งาน" @@ -911,40 +872,38 @@ itsOff: "ปิดใช้งาน" on: "เปิด" off: "ปิด" emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร" -unread: "ยังไม่ได้อ่าน" +unread: "ไม่ได้อ่าน" filter: "กรอง" controlPanel: "แผงควบคุม" manageAccounts: "จัดการบัญชี" -makeReactionsPublic: "ตั้งค่าประวัติการรีแอคชั่นเป็นสาธารณะ" -makeReactionsPublicDescription: "การทำเช่นนี้จะทำให้รายการรีแอคชั่นของคุณที่ผ่านมาทั้งหมดปรากฏต่อสาธารณะ" +makeReactionsPublic: "ตั้งค่าประวัติปฏิกิริยาต่อสาธารณะ" +makeReactionsPublicDescription: "การทำเช่นนี้จะทำให้รายการปฏิกิริยาที่ผ่านมาของคุณจะปรากฏต่อสาธารณะนะ" classic: "คลาสสิค" muteThread: "ปิดเสียงเธรด" -unmuteThread: "เลิกปิดเสียงเธรด" -followingVisibility: "การมองเห็นที่เรากำลังติดตาม" -followersVisibility: "การมองเห็นผู้ที่กำลังติดตามเรา" +unmuteThread: "เปิดเสียงเธรด" continueThread: "ดูความต่อเนื่องเธรด" deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?" incorrectPassword: "รหัสผ่านไม่ถูกต้อง" -voteConfirm: "ต้องการโหวต “{choice}” ใช่ไหม?" +voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?" hide: "ซ่อน" leaveGroup: "ออกจากกลุ่ม" leaveGroupConfirm: "คุณแน่ใจหรอว่าต้องการออกจาก \"{name}\"" -useDrawerReactionPickerForMobile: "แสดง ตัวจิ้มรีแอคชั่น เป็นแบบลิ้นชัก เมื่อใช้บนมือถือ" -welcomeBackWithName: "ยินดีต้อนรับการกลับมานะคะ, คุณ{name}" -clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์" +useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ" +welcomeBackWithName: "ยินดีต้อนรับการกลับมานะคะ, {name}" +clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ" overridedDeviceKind: "ประเภทอุปกรณ์" smartphone: "สมาร์ทโฟน" tablet: "แท็บเล็ต" auto: "อัตโนมัติ" -themeColor: "สีธีม" +themeColor: "อินสแตนซ์ Ticker Color" size: "ขนาด" numberOfColumn: "จำนวนคอลัมน์" searchByGoogle: "ค้นหา" -instanceDefaultLightTheme: "ธีมสว่างตามค่าเริ่มต้นของเซิร์ฟเวอร์" -instanceDefaultDarkTheme: "ธีมมืดตามค่าเริ่มต้นของเซิร์ฟเวอร์" +instanceDefaultLightTheme: "ธีมสว่างค่าเริ่มต้นสำหรับอินสแตนซ์" +instanceDefaultDarkTheme: "ธีมมืดค่าเริ่มต้นอินสแตนซ์" instanceDefaultThemeDescription: "ป้อนรหัสธีมในรูปแบบออบเจ็กต์" mutePeriod: "ระยะเวลาปิดเสียง" -period: "ระยะเวลา" +period: "สิ้นสุดการสำรวจความคิดเห็น" indefinitely: "ตลอดไป" tenMinutes: "10 นาที" oneHour: "1 ชั่วโมง" @@ -961,7 +920,7 @@ cropNo: "ใช้ตามที่เป็นอยู่" file: "ไฟล์" recentNHours: "ล่าสุด {n} ชั่วโมงที่แล้ว" recentNDays: "ล่าสุด {n} วันที่แล้ว" -noEmailServerWarning: "ยังไม่ได้ตั้งค่าเซิร์ฟเวอร์ของอีเมล" +noEmailServerWarning: "ไม่ได้กำหนดค่าเซิร์ฟเวอร์อีเมลนี้" thereIsUnresolvedAbuseReportWarning: "มีรายงานที่ยังไม่ได้แก้ไข" recommended: "แนะนำ" check: "ตรวจสอบ" @@ -974,29 +933,29 @@ deleteAccount: "ลบบัญชี" document: "เอกสาร" numberOfPageCache: "จำนวนหน้าเพจที่แคช" numberOfPageCacheDescription: "การเพิ่มจำนวนนี้จะช่วยเพิ่มความสะดวกให้กับผู้ใช้งาน แต่จะทำให้เซิร์ฟเวอร์โหลดมากขึ้นและต้องใช้หน่วยความจำมากขึ้นอีกด้วย" -logoutConfirm: "ต้องการออกจากระบบใช่ไหม?" -lastActiveDate: "ใช้งานล่าสุดเมื่อ" -statusbar: "แถบสถานะ" +logoutConfirm: "คุณแน่ใจว่าต้องการออกจากระบบ?" +lastActiveDate: "ใช้งานล่าสุดที่" +statusbar: "ไอคอนบนแถบสถานะ" pleaseSelect: "ตัวเลือก" -reverse: "พลิก" +reverse: "ย้อนกลับ" colored: "สี" -refreshInterval: "ความถี่ในการอัปเดต" +refreshInterval: "รอบการอัพเดต" label: "ป้ายชื่อ" type: "รูปแบบ" speed: "ความเร็ว" slow: "ช้า" fast: "เร็ว" -sensitiveMediaDetection: "การตรวจจับสื่อที่มีเนื้อหาละเอียดอ่อน" +sensitiveMediaDetection: "การตรวจจับของสื่อ NSFW" localOnly: "เฉพาะท้องถิ่น" -remoteOnly: "ระยะไกลเท่านั้น" +remoteOnly: "รีโมทเท่านั้น" failedToUpload: "การอัปโหลดล้มเหลว" cannotUploadBecauseInappropriate: "ไม่สามารถอัปโหลดไฟล์นี้ได้เนื่องจากระบบตรวจพบบางส่วนของไฟล์ว่านี้อาจจะเป็น NSFW" -cannotUploadBecauseNoFreeSpace: "ไม่สามารถอัปโหลดได้เนื่องจากไม่มีพื้นที่ว่างในไดรฟ์เหลือแล้ว" +cannotUploadBecauseNoFreeSpace: "การอัปโหลดนั้นล้มเหลวเนื่องจากไม่มีความจุของไดรฟ์" cannotUploadBecauseExceedsFileSizeLimit: "ไม่สามารถอัปโหลดไฟล์นี้ได้แล้วเนื่องจากเกินขีดจำกัดของขนาดไฟล์แล้ว" beta: "เบต้า" -enableAutoSensitive: "ทำเครื่องหมายว่ามีเนื้อหาที่ละเอียดอ่อนโดยอัตโนมัติ" -enableAutoSensitiveDescription: "อนุญาตให้ตรวจหาและทำเครื่องหมายสื่อว่ามีเนื้อหาโดยละเอียดอ่อนโดยอัตโนมัติ ผ่าน Machine Learning หากเป็นไปได้ แม้ว่าคุณจะปิดคุณสมบัตินี้ ก็อาจถูกตั้งค่าโดยอัตโนมัติ ทั้งนี้ขึ้นอยู่กับเซิร์ฟเวอร์" -activeEmailValidationDescription: "การตรวจสอบอีเมลของผู้ใช้จะเข้มงวดมากขึ้น โดยพิจารณาว่าเป็นอีเมลชั่วคราวหรือไม่ และสามารถติดต่อได้จริงหรือไม่ หากปิดการตรวจสอบนี้ จะตรวจสอบเพียงว่ารูปแบบอีเมลที่ถูกต้องหรือไม่เท่านั้น" +enableAutoSensitive: "ทำเครื่องหมาย NSFW อัตโนมัติ" +enableAutoSensitiveDescription: "อนุญาตให้ตรวจหาและทำเครื่องหมายสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่องหากเป็นไปได้ แม้ว่าตัวเลือกนี้จะถูกปิดใช้งาน แต่ก็สามารถเปิดใช้งานได้ทั้งอินสแตนซ์นี้" +activeEmailValidationDescription: "เปิดใช้งานการตรวจสอบที่อยู่อีเมลให้มีความเข้มงวดยิ่งขึ้น ซึ่งอาจจะรวมไปถึงการตรวจสอบที่อยู่อีเมล์ที่ใช้แล้วทิ้งและโดยให้พิจารณาว่าสามารถสื่อสารด้วยได้หรือไม่ เมื่อไม่เลือกระบบจะตรวจสอบเฉพาะรูปแบบของอีเมลเท่านั้น" navbar: "แถบนำทาง" shuffle: "สลับ" account: "บัญชีผู้ใช้" @@ -1005,35 +964,34 @@ pushNotification: "การแจ้งเตือนแบบพุช" subscribePushNotification: "เปิดการแจ้งเตือนแบบพุช" unsubscribePushNotification: "ปิดการแจ้งเตือนแบบพุช" pushNotificationAlreadySubscribed: "การแจ้งเตือนแบบพุชได้เปิดใช้งานแล้ว" -pushNotificationNotSupported: "เบราว์เซอร์หรือเซิร์ฟเวอร์ไม่รองรับการแจ้งเตือนแบบพุช" +pushNotificationNotSupported: "เบราว์เซอร์หรืออินสแตนซ์ของคุณนั้นไม่รองรับการแจ้งเตือนแบบพุช" sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว" -sendPushNotificationReadMessageCaption: "อาจทำให้อุปกรณ์ของคุณใช้พลังงานมากขึ้น" -windowMaximize: "ขยายใหญ่สุด" +sendPushNotificationReadMessageCaption: "การแจ้งเตือนที่มีข้อความ \"{emptyPushNotificationMessage}\" จะแสดงขึ้นมาในช่วงระยะเวลาสั้นๆ การดำเนินการนี้อาจทำให้เพิ่มการใช้งานแบตเตอรี่ของอุปกรณ์ถ้าหากมีนะ" +windowMaximize: "ขยายใหญ่สุดแล้ว" windowMinimize: "ย่อเล็กที่สุด" windowRestore: "เลิกทำ" -caption: "คำอธิบาย" +caption: "รายละเอียด" loggedInAsBot: "ล็อกอินเป็นบอตอยู่ในขณะนี้" tools: "เครื่องมือ" cannotLoad: "ไม่สามารถโหลดได้" numberOfProfileView: "มุมมองโปรไฟล์" -like: "ถูกใจ!" -unlike: "เลิกถูกใจ" -numberOfLikes: "จำนวนยอดถูกใจ" +like: "ชื่นชอบ" +unlike: "ไม่ชอบ" +numberOfLikes: "จำนวนไลค์" show: "แสดงผล" neverShow: "ไม่ต้องแสดงข้อความนี้อีก" remindMeLater: "ไว้ครั้งหน้าแล้วกัน" -didYouLikeMisskey: "คุณชอบ CherryPick ไหม?" +didYouLikeMisskey: "คุณเคยชอบ CherryPick ไหม?" pleaseDonate: "CherryPick เป็นซอฟต์แวร์ฟรีที่ใช้งานโดย {host} เราขอขอบคุณการสนับสนุนของคุณอย่างสูงเพื่อให้การพัฒนา CherryPick สามารถดำเนินต่อไปได้!" -correspondingSourceIsAvailable: "ซอร์สโค้ดที่เกี่ยวข้องมีอยู่ที่ {anchor}" roles: "บทบาท" role: "บทบาท" noRole: "ไม่พบบทบาท" normalUser: "ผู้ใช้มาตรฐาน" undefined: "ไม่ได้กำหนด" -assign: "มอบหมาย" -unassign: "เลิกมอบหมาย" +assign: "กำหนด" +unassign: "ยังไม่มอบหมาย" color: "สี" -manageCustomEmojis: "จัดการเอโมจิที่กำหนดเอง" +manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" manageAvatarDecorations: "จัดการตกแต่งอวตาร" youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ" cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว" @@ -1048,39 +1006,33 @@ achievements: "ความสำเร็จ" gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง" gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ" thisPostMayBeAnnoying: "โน้ตนี้อาจจะเป็นการรบกวนผู้อื่นนะคะ" -thisPostMayBeAnnoyingHome: "โพสต์ไปยังไทม์ไลน์หลัก" +thisPostMayBeAnnoyingHome: "โพสต์ไปยังบ้านไทม์ไลน์" thisPostMayBeAnnoyingCancel: "เลิก" thisPostMayBeAnnoyingIgnore: "โพสต์ยังไงก็แล้วแต่" -collapseRenotes: "ยุบรีโน้ตที่คุณเคยเห็นแล้ว" -collapseRenotesDescription: "พับย่อโน้ตที่เคยตอบสนองหรือรีโน้ตแล้ว" +collapseRenotes: "ยุบ renotes ที่คุณได้เห็นแล้ว" internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด" -internalServerErrorDescription: "เกิดข้อผิดพลาดที่ไม่คาดคิดภายในเซิร์ฟเวอร์" +internalServerErrorDescription: "เซิร์ฟเวอร์รันค้นพบข้อผิดพลาดที่ไม่คาดคิด" copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด" -joinThisServer: "ลงทะเบียนบนเซิร์ฟเวอร์นี้" -exploreOtherServers: "มองหาเซิร์ฟเวอร์อื่น" -letsLookAtTimeline: "มาดูไทม์ไลน์กัน" -disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?" +joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้" +exploreOtherServers: "มองหาอินสแตนซ์อื่น" +letsLookAtTimeline: "ลองดูที่ไทม์ไลน์" +disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?" disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น" disableFederationOk: "ปิดการใช้งาน" -invitationRequiredToRegister: "เซิร์ฟเวอร์นี้เป็นแบบรับเชิญ เฉพาะผู้มีรหัสเชิญเท่านั้นถึงสามารถลงทะเบียนได้" -emailNotSupported: "เซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล" +invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญ เพื่องลงทะเบียนเข้าใช้งาน" +emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมล" postToTheChannel: "โพสต์ลงช่อง" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" reactionAcceptance: "การยอมรับรีแอคชั่น" -likeOnly: "ที่ถูกใจเท่านั้น" -likeOnlyForRemote: "ทั้งหมด (เฉพาะการถูกใจจากเซิร์ฟเวอร์ระยะไกล)" -nonSensitiveOnly: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน (เฉพาะการถูกใจจากระยะไกลเท่านั้น)" +likeOnly: "ที่ชอบเท่านั้น" +likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น" +nonSensitiveOnly: "ไม่มีความอ่อนไหวเท่านั้น" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "ไม่มีความอ่อนไหวเท่านั้น (เฉพาะไลค์จากระยะไกลเท่านั้น)" rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน" -resetPasswordConfirm: "ต้องการรีเซ็ตรหัสผ่านใช่ไหม?" -sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน" -sensitiveWordsDescription: "โน้ตที่มีคำที่ระบุไว้จะถูกตั้งค่าการมองเห็นของให้แสดงเฉพาะในหน้าหลักเท่านั้น คั่นคำด้วยการขึ้นบรรทัดใหม่" -sensitiveWordsDescription2: "ถ้าแยกด้วยเว้นวรรคจะเป็นการระบุ AND และถ้าล้อมคำด้วยสแลช (/) จะเป็นการใช้ regular expression" -prohibitedWords: "คำต้องห้าม" -prohibitedWordsDescription: "จะแจ้งเตือนว่าเกิดข้อผิดพลาดเมื่อพยายามโพสต์โน้ตที่มีคำที่กำหนดไว้ สามารถตั้งได้หลายคำด้วยการขึ้นบรรทัดใหม่" -prohibitedWordsDescription2: "ถ้าแยกด้วยเว้นวรรคจะเป็นการระบุ AND และถ้าล้อมคำด้วยสแลช (/) จะเป็นการใช้ regular expression" -hiddenTags: "แฮชแท็กที่ซ่อนอยู่" -hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่" +resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?" +sensitiveWords: "คำที่ละเอียดอ่อน" +sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ" +sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ" notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน" license: "ใบอนุญาต" unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?" @@ -1090,143 +1042,119 @@ retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหม retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?" retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ" enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล" -enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล" -showClipButtonInNoteFooter: "เพิ่ม “คลิป” ไปยังเมนูสั่งการของโน้ต" -reactionsDisplaySize: "ขนาดของรีแอคชั่น" -limitWidthOfReaction: "จำกัดความกว้างสูงสุดของรีแอคชั่นและแสดงให้เล็กลง" -noteIdOrUrl: "ID ของโน้ต หรือ URL" +enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล" +showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน" +reactionsDisplaySize: "รีแอคชั่นแสดงผลขนาด" +noteIdOrUrl: "โน้ต ID หรือ URL" video: "วีดีโอ" videos: "วีดีโอ" -audio: "เสียง" -audioFiles: "เสียง" dataSaver: "ประหยัดข้อมูล" -accountMigration: "โยกย้ายบัญชี" +accountMigration: "การโยกย้ายบัญชี" accountMoved: "ผู้ใช้รายนี้ได้ย้ายไปยังบัญชีใหม่แล้ว:" accountMovedShort: "บัญชีนี้ถูกโอนย้ายไปแล้วค่ะ" -operationForbidden: "การดำเนินการถูกห้าม" +operationForbidden: "ห้ามดำเนินการ" forceShowAds: "แสดงโฆษณาเสมอ" -addMemo: "เพิ่มเมโม" -editMemo: "แก้ไขเมโม" -reactionsList: "รายการรีแอคชั่น" -renotesList: "รายการรีโน้ต" -notificationDisplay: "การแสดงการแจ้งเตือน" +addMemo: "เพิ่มมีโม" +editMemo: "แก้ไขมีโม" +reactionsList: "ปฏิกิริยา" +renotesList: "Renotes รีโน้ต" +notificationDisplay: "การแจ้งเตือน" leftTop: "บนซ้าย" rightTop: "บนขวา" leftBottom: "ล่างซ้าย" rightBottom: "ล่างขวา" stackAxis: "ทิศทางการซ้อน" vertical: "แนวตั้ง" -horizontal: "แนวนอน" +horizontal: "ด้านข้าง" position: "ตำแหน่ง" -serverRules: "กฎของเซิร์ฟเวอร์" -pleaseConfirmBelowBeforeSignup: "หากต้องการลงทะเบียนบนเซิร์ฟเวอร์นี้ คุณต้องตรวจสอบและยอมรับสิ่งต่อไปนี้" +serverRules: "กฎของเซิฟเวอร์" +pleaseConfirmBelowBeforeSignup: "โปรดยืนยันที่ด้านล่างก่อนสมัครใช้งาน" pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ" continue: "ดำเนินการต่อ" preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้" -preservedUsernamesDescription: "ระบุชื่อผู้ใช้ที่จะสงวนชื่อไว้ คั่นด้วยการขึ้นบรรทัดใหม่ ชื่อผู้ใช้ที่ระบุที่นี่จะไม่สามารถใช้งานได้อีกต่อไปเมื่อสร้างบัญชีใหม่ ยกเว้นเมื่อผู้ดูแลระบบสร้างบัญชี นอกจากนี้ บัญชีที่มีอยู่แล้วจะไม่ได้รับผลกระทบ" +preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร" createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้" archive: "เก็บถาวร" -archived: "เก็บถาวรแล้ว" -unarchive: "เลิกการเก็บถาวร" -channelArchiveConfirmTitle: "ต้องการเก็บถาวรเจ้า {name} ใช่ไหม?" -channelArchiveConfirmDescription: "เมื่อเก็บถาวรแล้ว จะไม่ปรากฏในรายการช่องหรือผลการค้นหาอีกต่อไป และจะไม่สามารถโพสต์ใหม่ได้อีกต่อไป" +channelArchiveConfirmTitle: "เก็บถาวรจริงๆ {name} มั้ย?" +channelArchiveConfirmDescription: "ช่องที่ถูกเก็บถาวรแล้วนั้นจะไม่ปรากฏในรายการช่องหรือผลการค้นหานั้นอีกต่อไปไม่สามารถเพิ่มโพสต์ใหม่ได้อีกต่อไปนะ" thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ" displayOfNote: "การแสดงโน้ต" initialAccountSetting: "ตั้งค่าโปรไฟล์" youFollowing: "ติดตามแล้ว" -preventAiLearning: "ปฏิเสธการเรียนรู้ด้วย generative AI" -preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว" +preventAiLearning: "ปฏิเสธการใช้งาน ในการเรียนรู้ของเครื่อง (Generative AI)" +preventAiLearningDescription: "การส่งคำร้องขอโปรแกรมรวบรวมข้อมูลไม่ให้ใช้ข้อความที่โพสต์หรือรูปภาพ ฯลฯ ในชุดข้อมูลแมชชีนเลิร์นนิง (Predictive / Generative AI) สิ่งนี้นั้นทำได้โดยการเพิ่มแฟล็กการตอบสนอง \"noai\" HTML ให้กับเนื้อหาที่เกี่ยวข้อง แต่อย่างไรก็ตามแล้ว การป้องกันโดยสมบูรณ์นั้นไม่สามารถทำได้ผ่านแฟล็กนี้เนื่องจากอาจจะทำให้ถูกเพิกเฉยได้" options: "ตัวเลือกบทบาท" specifyUser: "ผู้ใช้เฉพาะ" -lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?" -openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?" -specifyHost: "ระบุโฮสต์" failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้" update: "อัปเดต" -rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้เอโมจินี้เป็นรีแอคชั่นได้" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ถ้าหากไม่ได้ระบุบทบาท ใคร ๆ ก็สามารถใช้เอโมจินี้เพื่อรีแอคชั่นได้" +rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้อิโมจินี้เป็นรีแอคชั่นได้" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ถ้าหากไม่ได้ระบุบทบาท ทุกคนนั้นก็สามารถใช้อิโมจินี้เป็นการแสดงความรู้สึกได้นะ" rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "บทบาทเหล่านี้ต้องเป็นสาธารณะ" -cancelReactionConfirm: "ต้องการลบรีแอคชั่นใช่ไหม?" -changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นใช่ไหม?" +cancelReactionConfirm: "ต้องการลบรีแอคชั่นของคุณจริงๆหรอ?" +changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นของคุณจริงๆหรอ?" later: "ไว้ทีหลัง" goToMisskey: "ถึง CherryPick" -additionalEmojiDictionary: "พจนานุกรมเอโมจิเพิ่มเติม" +additionalEmojiDictionary: "พจนานุกรมอีโมจิเพิ่มเติม" installed: "ติดตั้งแล้ว" branding: "แบรนดิ้ง" enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์" enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ" turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้" -createInviteCode: "สร้างรหัสเชิญ" +createInviteCode: "สร้างคำเชิญ" createWithOptions: "สร้างด้วยตัวเลือก" -createCount: "จำนวนรหัสเชิญ" -inviteCodeCreated: "สร้างรหัสเชิญแล้ว" -inviteLimitExceeded: "จำนวนรหัสเชิญที่สามารถสร้างได้ถึงขีดจำกัดแล้ว" -createLimitRemaining: "รหัสเชิญที่สามารถสร้างได้: เหลืออยู่ {limit} รหัส" -inviteLimitResetCycle: "สามารถสร้างรหัสเชิญได้อีกสูงสุด {limit} รหัส ภายใน {time}" +createCount: "จำนวนการเชิญ" +inviteCodeCreated: "สร้างคำเชิญแล้ว" +inviteLimitExceeded: "คุณสร้างคำเชิญเกินถึงขีดจำกัดแล้วนะ" +createLimitRemaining: "ขีดจำกัดการเชิญ: {limit} ที่เหลืออยู่" +inviteLimitResetCycle: "ขีดจำกัดนี้จะถูกรีเซ็ตเป็น {limit} ที่ {time}." expirationDate: "วันที่หมดอายุ" noExpirationDate: "ไม่มีหมดอายุ" -inviteCodeUsedAt: "วันเวลาที่ใช้รหัสเชิญ" -registeredUserUsingInviteCode: "ผู้ใช้ที่ใช้รหัสเชิญ" +inviteCodeUsedAt: "รหัสคำเชิญใช้แล้วที่" +registeredUserUsingInviteCode: "ใช้คำเชิญแล้วโดย" waitingForMailAuth: "กำลังรอการยืนยันอีเมล" -inviteCodeCreator: "ผู้ใช้ที่สร้างรหัสเชิญ" -usedAt: "วันเวลาที่ถูกใช้" -unused: "ยังไม่ได้ใช้" -used: "ถูกใช้แล้ว" +inviteCodeCreator: "สร้างการเชิญแล้วโดย" +usedAt: "ใช้แล้วที่" +unused: "ไม่ใช้แล้ว" +used: "ใช้แล้ว" expired: "หมดอายุแล้ว" -doYouAgree: "ยอมรับไหม?" +doYouAgree: "ยอมรับมั้ย?" beSureToReadThisAsItIsImportant: "กรุณาอ่านข้อมูลที่สำคัญอันนี้" -iHaveReadXCarefullyAndAgree: "ฉันได้อ่านและยินยอมเนื้อหาของ “{x}”" +iHaveReadXCarefullyAndAgree: "ฉันได้อ่านข้อความ \"{x}\" และยินยอม" dialog: "ไดอะล็อก" icon: "ไอคอน" forYou: "สำหรับคุณ" currentAnnouncements: "ประกาศในปัจจุบัน" pastAnnouncements: "ประกาศที่ผ่านมา" youHaveUnreadAnnouncements: "มีการประกาศที่ยังไม่ได้อ่าน" -useSecurityKey: "โปรดปฏิบัติตามคำแนะนำของเบราว์เซอร์หรืออุปกรณ์ของคุณเพื่อใช้ security key หรือ passkey" replies: "ตอบกลับ" renotes: "รีโน้ต" loadReplies: "แสดงการตอบกลับ" loadConversation: "แสดงบทสนทนา" -pinnedList: "รายชื่อที่ปักหมุดไว้" -keepScreenOn: "เปิดหน้าจออุปกรณ์ค้างไว้" -verifiedLink: "ความเป็นเจ้าของลิงก์ได้รับการยืนยันแล้ว" +pinnedList: "รายการที่ปักหมุดไว้แล้ว" +keepScreenOn: "เปิดหน้าจอไว้" notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต์ใหม่" unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่" authentication: "การตรวจสอบสิทธิ์" -authenticationRequiredToContinue: "กรุณายืนยันตัวตนทางอิเล็กทรอนิกส์เพื่อดำเนินการต่อ" -dateAndTime: "วันเวลา" +authenticationRequiredToContinue: "กรุณาตรวจสอบการรับรองความถูกต้องเพื่อดำเนินการต่อ" +dateAndTime: "เวลาประทับ" showRenotes: "แสดงรีโน้ต" edited: "แก้ไขแล้ว" notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน" mutualFollow: "ติดตามซึ่งกันและกัน" -followingOrFollower: "กำลังติดตามหรือผู้ติดตาม" fileAttachedOnly: "เฉพาะโน้ตที่มีไฟล์เท่านั้น" -showRepliesToOthersInTimeline: "แสดงการตอบกลับผู้อื่นลงในไทม์ไลน์" -hideRepliesToOthersInTimeline: "ไม่แสดงการตอบกลับผู้อื่นลงในไทม์ไลน์" -showRepliesToOthersInTimelineAll: "รวมตอบกลับจากทุกคนที่คุณติดตามไว้ในไทม์ไลน์ของคุณ" -hideRepliesToOthersInTimelineAll: "ซ่อนตอบกลับจากทุกคนที่คุณติดตามไปจากไทม์ไลน์ของคุณ" -confirmShowRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการแสดงการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ ใส่ลงไทม์ไลน์ใช่ไหม?" -confirmHideRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการซ่อนการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ ไปจากไทม์ไลน์ใช่ไหม?" +showRepliesToOthersInTimeline: "แสดงการตอบกลับไปยังอื่นๆในไทม์ไลน์" +hideRepliesToOthersInTimeline: "ซ่อนการตอบกลับไปยังอื่นๆจากไทม์ไลน์" externalServices: "บริการภายนอก" -sourceCode: "ซอร์สโค้ด" -sourceCodeIsNotYetProvided: "ซอร์สโค้ดยังไม่พร้อมใช้งาน โปรดติดต่อผู้ดูแลระบบเพื่อแก้ไขปัญหานี้" -repositoryUrl: "URL ของ repository" -repositoryUrlDescription: "หากมีที่เก็บซอร์สโค้ดที่เปิดเผยต่อสาธารณะ ให้ป้อน URL ที่เก็บซอร์สโค้ดนั้น แต่หากคุณใช้ CherryPick ตามต้นฉบับ (ไม่มีการเปลี่ยนแปลงซอร์สโค้ด) ให้ป้อน https://github.com/kokonect-link/cherrypick" -repositoryUrlOrTarballRequired: "หากคุณไม่มี repository สาธารณะ คุณจะต้องจัดเตรียม tarball แทน ดู .config/example.yml สำหรับรายละเอียด" -feedback: "ฟีดแบ็ก" -feedbackUrl: "URLของฟีดแบ็ก" impressum: "อิมเพรสชั่น" impressumUrl: "URL อิมเพรสชั่น" -impressumDescription: "การติดป้ายกำกับ (Impressum) มีผลบังคับใช้ในบางประเทศและภูมิภาค เช่น ประเทศเยอรมนี" privacyPolicy: "นโยบายความเป็นส่วนตัว" privacyPolicyUrl: "URL นโยบายความเป็นส่วนตัว" tosAndPrivacyPolicy: "เงื่อนไขในการให้บริการและนโยบายความเป็นส่วนตัว" avatarDecorations: "การตกแต่งอวตาร" attach: "แนบ" detach: "นำออก" -detachAll: "เอาออกทั้งหมด" angle: "แองเกิล" -flip: "พลิก" +flip: "ย้อนกลับ" showAvatarDecorations: "แสดงตกแต่งอวตาร" releaseToRefresh: "ปล่อยเพื่อรีเฟรช" refreshing: "กำลังรีเฟรช..." @@ -1234,99 +1162,29 @@ pullDownToRefresh: "ดึงลงเพื่อรีเฟรช" disableStreamingTimeline: "ปิดใช้งานอัปเดตไทม์ไลน์แบบเรียลไทม์" useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว" signupPendingError: "มีปัญหาในการตรวจสอบที่อยู่อีเมลลิงก์อาจหมดอายุแล้ว" -cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย" doReaction: "เพิ่มรีแอคชั่น" -code: "โค้ด" -reloadRequiredToApplySettings: "จำเป็นต้องมีการโหลดซ้ำเพื่อให้การตั้งค่ามีผล" -remainingN: "เหลือ : {n}" -overwriteContentConfirm: "แน่ใจหรือไม่ว่าต้องการเขียนทับเนื้อหาปัจจุบัน?" -seasonalScreenEffect: "เอฟเฟกต์หน้าจอตามฤดูกาล" -decorate: "ตกแต่ง" -addMfmFunction: "เพิ่มการตกแต่ง" -enableQuickAddMfmFunction: "แสดงตัวจิ้มเลือก MFM ขั้นสูง" -bubbleGame: "เกมบับเบิ้ล" -sfx: "เสียงเอฟเฟ็กต์" -soundWillBePlayed: "จะมีการเล่นเอฟเฟกต์เสียง" -showReplay: "ดูรีเพลย์" -replay: "รีเพลย์" -replaying: "กำลังรีเพลย์" -endReplay: "ออกจากรีเพลย์" -copyReplayData: "คัดลอกข้อมูลรีเพลย์" -ranking: "อันดับ" -lastNDays: "ล่าสุด {n} วันที่แล้ว" -backToTitle: "กลับไปหน้าไตเติ้ล" -hemisphere: "พื้นที่ที่อาศัยอยู่" -withSensitive: "แสดงโน้ตที่มีไฟล์เนื้อหาละเอียดอ่อน" -userSaysSomethingSensitive: "โพสต์ที่มีไฟล์เนื้อหาละเอียดอ่อนของ {name}" -enableHorizontalSwipe: "ปัดเพื่อสลับแท็บ" -loading: "กำลังโหลด" -surrender: "ยอมแพ้" -gameRetry: "เริ่มเกมใหม่" -notUsePleaseLeaveBlank: "หากไม่ได้ใช้กรุณาเว้นว่างไว้" -useTotp: "ใช้รหัสผ่านแบบใช้ครั้งเดียว (TOTP)" -useBackupCode: "ใช้รหัสแบ๊กอัป" -launchApp: "เริ่มแอป" -useNativeUIForVideoAudioPlayer: "ใช้ UI ของเบราว์เซอร์เพื่อเล่นวิดีโอ/เสียง" -keepOriginalFilename: "คงชื่อไฟล์เดิมไว้" -keepOriginalFilenameDescription: "หากปิดการตั้งค่านี้ ในระหว่างการอัปโหลดชื่อไฟล์จะถูกแทนที่ด้วยสตริงแบบสุ่มโดยอัตโนมัติ" -noDescription: "ไม่มีข้อความอธิบาย" -alwaysConfirmFollow: "แสดงข้อความยืนยันเมื่อกดติดตาม" -inquiry: "ติดต่อเรา" -tryAgain: "โปรดลองอีกครั้ง" -confirmWhenRevealingSensitiveMedia: "ตรวจสอบก่อนแสดงสื่อที่มีเนื้อหาละเอียดอ่อน" -sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?" -createdLists: "รายชื่อที่ถูกสร้าง" -createdAntennas: "เสาอากาศที่ถูกสร้าง" -_delivery: - status: "สถานะการจัดส่ง" - stop: "ระงับการส่ง" - resume: "จัดส่งต่อ" - _type: - none: "กำลังเผยแพร่" - manuallySuspended: "หยุดชั่วคราวด้วยตนเอง" - goneSuspended: "เซิร์ฟเวอร์ถูกระงับเนื่องจากมีการลบเซิร์ฟเวอร์นี้" - autoSuspendedForNotResponding: "เซิร์ฟเวอร์ถูกระงับเนื่องจากไม่ตอบสนอง" -_bubbleGame: - howToPlay: "วิธีเล่น" - hold: "ถือไว้" - _score: - score: "คะแนน" - scoreYen: "จำนวนเงินที่ได้รับ" - highScore: "คะแนนสูงสุด" - maxChain: "จำนวน chain สูงสุด" - yen: "{yen} เยน" - estimatedQty: "{qty} อัน" - scoreSweets: "โอนิงิริ {onigiriQtyWithUnit}" - _howToPlay: - section1: "ขยับตำแหน่งและวางวัตถุลงในกล่อง" - section2: "เมื่อวัตถุประเภทเดียวกันมารวมกัน พวกมันจะกลายเป็นวัตถุใหม่และคุณจะได้รับคะแนน" - section3: "หากวัตถุล้นออกมาจากกล่อง เกมก็จะจบลง ตั้งเป้าทำคะแนนให้สูงด้วยการหลอมวัตถุต่าง ๆ โดยไม่ทำให้ล้นกล่อง!" _announcement: - forExistingUsers: "ผู้ใช้งานที่มีอยู่ตอนนี้เท่านั้น" - forExistingUsersDescription: "หากเปิดใช้งาน การประกาศนี้จะแสดงเฉพาะกับผู้ใช้ที่สร้างบัญชีก่อน/ที่มีอยู่ในขณะที่สร้างประกาศนี้เท่านั้น หากปิดใช้งาน การประกาศนี้จะแสดงกับผู้ใช้ที่สร้างบัญชีหลังจากสร้างประกาศนี้ด้วย" - needConfirmationToRead: "จำเป็นต้องยืนยันว่าอ่านแล้ว" - needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว นอกจากนี้ยังทำให้ประกาศนี้ยังไม่ถูกอ่านเมื่อใช้ฟังก์ชั่น “ทำเครื่องหมายฯ ทั้งหมดว่าอ่านแล้ว”" - end: "เก็บประกาศ" - tooManyActiveAnnouncementDescription: "เนื่องจากมีการประกาศที่ยังใช้งานอยู่จำนวนมาก อาจทำให้ UX ลดลง แนะนำให้พิจารณาการเก็บประกาศที่สิ้นสุดไปแล้ว" - readConfirmTitle: "ทำเครื่องหมายว่าอ่านแล้วเลยไหม?" - readConfirmText: "จะทำเครื่องหมายใส่ “{title}” ว่าอ่านแล้ว" - shouldNotBeUsedToPresentPermanentInfo: "เนื่องจากมีความเป็นไปได้สูงที่จะส่งผลเสียต่อง UX ของผู้ใช้ใหม่ จึงขอแนะนำให้ใช้ประกาศสำหรับข้อมูลที่ต้องการการตอบสนองในทันที ไม่ใช่ข้อมูลที่ต้องการแสดงตลอดเวลา" - dialogAnnouncementUxWarn: "เราขอแนะนำให้ใช้ด้วยความระมัดระวัง เนื่องจากการแจ้งเตือนแบบกล่องโต้ตอบตั้งแต่ 2 รายการขึ้นไปพร้อมกันอาจส่งผลเสียต่อ UX ได้อย่างมาก" + forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น" + forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน" + needConfirmationToRead: "จำเป็นต้องยืนยันเพื่อทำเครื่องหมายบอกว่าอ่านแล้ว" + needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\"" + end: "ประกาศเก็บถาวร" + tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ" + readConfirmTitle: "ทำเครื่องหมายบอกว่าอ่านแล้วเลยมั้ย?" + readConfirmText: "การดำเนินการนี้จะทำเครื่องหมายเนื้อหาของ \"{title}\" บอกว่าอ่านแล้วนะ" silence: "ไม่มีการแจ้งเตือน" - silenceDescription: "หากเปิดใช้งาน จะไม่มีการแจ้งเตือนประกาศนี้ และผู้ใช้จะไม่จำเป็นต้องทำเครื่องหมายว่าอ่านแล้ว" _initialAccountSetting: - accountCreated: "สร้างบัญชีเสร็จสมบูรณ์!" + accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!" letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ" letsFillYourProfile: "ก่อนอื่นมาตั้งค่าโปรไฟล์ของคุณ" profileSetting: "ตั้งค่าโปรไฟล์" privacySetting: "ตั้งค่าความเป็นส่วนตัว" theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ" - youCanEditMoreSettingsInSettingsPageLater: "สามารถตั้งค่าเพิ่มเติมได้ที่หน้า “การตั้งค่า” อย่าลืมไปเยี่ยมชมภายหลังด้วย" - followUsers: "ลองติดตามผู้ใช้ที่สนใจเพื่อสร้างไทม์ไลน์ดูสิ" + youCanEditMoreSettingsInSettingsPageLater: "ยังมีการตั้งค่าอื่นๆ อีกมากมายที่คุณนั้นสามารถกำหนดค่าได้จาก \"การตั้งค่า\" เพื่อให้แน่ใจว่าได้เยี่ยมชมมันได้ภายหลังนะ" + followUsers: "ลองติดตามผู้ใช้บางคนที่คุณอาจจะสนใจเพื่อสร้างไทม์ไลน์ของคุณสิ !" pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ" initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!" - haveFun: "ขอให้สนุกกับ {name}!" - youCanContinueTutorial: "คุณสามารถดำเนินการต่อด้วยบทช่วยสอนเกี่ยวกับวิธีใช้ {name} (CherryPick) หรือออกจากบทช่วยสอนแล้วเริ่มใช้งานได้ทันที" + haveFun: "ขอให้สนุก {name}!" startTutorial: "เริ่มการฝึกสอน" skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?" laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?" @@ -1337,130 +1195,77 @@ _initialTutorial: skipAreYouSure: "ต้องการออกจากบทช่วยสอนใช่ไหม?" _landing: title: "ยินดีต้อนรับสู่บทช่วยสอน" - description: "คุณสามารถตรวจสอบการใช้งานและฟังก์ชั่นพื้นฐานของ CherryPick ได้ที่นี่" _note: title: "โน้ตคืออะไร?" - description: "โพสต์ใน CherryPick เรียกว่า “โน้ต” ซึ่งจะจัดเรียงตามลำดับเวลาบนไทม์ไลน์และอัปเดตแบบเรียลไทม์" - reply: "คุณสามารถตอบกลับได้ และคุณยังสามารถตอบกลับใส่การตอบกลับเพื่อสนทนาต่อได้เสมือนดั่งเธรด" - renote: "คุณสามารถแชร์โน้ตไปยังไทม์ไลน์ของคุณเอง คุณยังสามารถเพิ่มข้อความและเครื่องหมายคำพูดได้" - reaction: "คุณสามารถเพิ่มรีแอคชั่นได้ รายละเอียดจะอธิบายอยู่ในหน้าถัดไป" - menu: "คุณสามารถดูรายละเอียดโน้ต คัดลอกลิงก์ และดำเนินการอื่นๆ ได้" _reaction: title: "รีแอคชั่นคืออะไร?" - description: "โน้ตสามารถ“รีแอคชั่น”ด้วยเอโมจิต่างๆ ซึ่งทำให้สามารถแสดงความแตกต่างเล็กๆ น้อยๆ ที่อาจไม่สามารถสื่อออกมาได้ด้วยการแค่การกด “ถูกใจ”" - letsTryReacting: "คุณสามารถเพิ่มรีแอคชั่นได้ด้วยการคลิกปุ่ม “+” บนโน้ต ลองรีแอคชั่นโน้ตตัวอย่างนี้ดูสิ!" - reactToContinue: "เพิ่มรีแอคชั่นเพื่อดำเนินการต่อ" - reactNotification: "คุณจะได้รับการแจ้งเตือนแบบเรียลไทม์เมื่อมีคนตอบรีแอคชั่นโน้ตของคุณ" - reactDone: "คุณสามารถยกเลิกรีแอคชั่นได้โดยการกดปุ่ม “-”" _timeline: title: "แนวคิดเรื่องของไทม์ไลน์" - description1: "CherryPick มีหลายไทม์ไลน์ขึ้นอยู่กับวิธีการใช้งานของคุณ (บางไทม์ไลน์อาจไม่สามารถใช้ได้ขึ้นอยู่กับนโยบายของเซิร์ฟเวอร์)" - home: "คุณสามารถดูโพสต์จากบัญชีที่คุณติดตามได้" - local: "คุณสามารถดูโพสต์จากผู้ใช้ทั้งหมดบนเซิร์ฟเวอร์นี้" - social: "จะแสดงโพสต์ทั้งจากไทม์ไลน์หลักและไทม์ไลน์ท้องถิ่น" - global: "คุณสามารถดูโพสต์จากเซิร์ฟเวอร์ที่เชื่อมต่ออื่นๆ ทั้งหมดได้" - description2: "คุณสามารถสลับระหว่างแต่ละไทม์ไลน์ได้ตลอดเวลาได้ที่บริเวณด้านบนของหน้าจอ" - description3: "นอกจากนี้ยังมีรายการไทม์ไลน์ ไทม์ไลน์ของช่อง ฯลฯ โปรดดู {link} สำหรับรายละเอียดเพิ่มเติม" _postNote: - title: "ตั้งค่าการโพสต์โน้ต" - description1: "เมื่อโพสต์โน้ตบน CherryPick คุณสามารถตั้งค่าตัวเลือกต่างๆ ได้ แบบฟอร์มการส่งมีลักษณะดังนี้" + title: "ตั้งค่ากำลังโพสต์โน้ต" _visibility: description: "คุณสามารถจำกัดผู้ที่สามารถดูโน้ตของคุณได้นะ" public: "โน้ตของคุณนั้นจะปรากฏแก่ผู้ใช้งานทุกคน" - home: "เผยแพร่บนไทม์ไลน์หลักเท่านั้น แต่ผู้ติดตาม ผู้ที่เข้ามาดูโปรไฟล์ และผู้ที่เห็นจากรีโน้ตยังสามารถดูโพสต์นี้ได้" - followers: "มองเห็นได้เฉพาะผู้ติดตามเท่านั้น ไม่มีใครอื่นนอกจากตัวคุณเองที่สามารถรีโน้ตได้ และมีเพียงผู้ติดตามของคุณเท่านั้นที่สามารถดูได้" - direct: "เปิดให้เห็นเฉพาะผู้ใช้ที่ระบุเท่านั้น และพวกเขาจะได้รับแจ้งเตือนด้วย คุณสามารถใช้มันแทนข้อความโดยตรง (dm)" - doNotSendConfidencialOnDirect1: "โปรดใช้ความระมัดระวังในการส่งข้อมูลที่ละเอียดอ่อน" - doNotSendConfidencialOnDirect2: "ผู้ดูแลระบบเซิร์ฟเวอร์ปลายทางสามารถดูเนื้อหาที่โพสต์ได้ ดังนั้นหากคุณส่งโพสต์โดยตรงไปยังผู้ใช้บนเซิร์ฟเวอร์ที่ไม่น่าเชื่อถือ คุณจะต้องใช้ความระมัดระวังในการจัดการข้อมูลที่เป็นความลับ" - localOnly: "การโพสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวอร์อื่น ผู้ใช้บนเซิร์ฟเวอร์อื่นจะไม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงการตั้งค่าการแสดงผลข้างต้น" _cw: title: "คำเตือนเกี่ยวกับเนื้อหา" - description: "เนื้อหาที่เขียนด้วย “คำอธิบายประกอบ” จะแสดงแทนข้อความหลัก คลิก “ดูเพิ่มเติม” เพื่อแสดงข้อความเต็ม" _exampleNote: cw: "นี่อาจจะทำให้คุณหิวอย่างแน่นอน!" - note: "เพิ่งไปกินโดนัทเคลือบช็อคโกแลตมา 🍩😋" - useCases: "ใช้สิ่งนี้เพื่อระบุโน้ตที่ต้องตามแนวทางปฏิบัติของเซิร์ฟเวอร์ หรือเพื่อควบคุมการสปอยล์และข้อความที่ละเอียดอ่อนด้วยตนเอง" - _howToMakeAttachmentsSensitive: - title: "จะทำเครื่องหมายไฟล์แนบว่ามีเนื้อหาละเอียดอ่อนได้อย่างไร?" - description: "ทำเครื่องหมายไฟล์แนบว่า “มีเนื้อหาละเอียดอ่อน” เมื่อจำเป็นตามแนวทางของเซิร์ฟเวอร์ หรือเมื่อไฟล์แนบไม่ควรปรากฏให้เห็น" - tryThisFile: "ลองทำให้รูปภาพที่แนบมากับแบบฟอร์มนี้มีเนื้อหาละเอียดอ่อน!" - _exampleNote: - note: "อุ้ย นัตโตะ ฝาเปิดเละเทะ..." - method: "หากต้องการทำให้ไฟล์แนบมีเนื้อหาละเอียดอ่อน ให้คลิกไฟล์เพื่อเปิดเมนูแล้วคลิก “ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน”" - sensitiveSucceeded: "เมื่อแนบไฟล์ โปรดตั้งค่าเครื่องหมายว่ามีเนื้อหาละเอียดอ่อนตามแนวทางของเซิร์ฟเวอร์" - doItToContinue: "ทำเครื่องหมายกับรูปภาพว่ามีเนื้อหาละเอียดอ่อน เพื่อดำเนินการต่อ" - _done: - title: "บทเรียนจบลงแล้วจ้า เย่เย่เย่ 🎉" - description: "คุณสมบัติที่แนะนำในที่นี่เป็นเพียงบางส่วนเท่านั้น หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ CherryPick โปรดไปที่ {link}" -_timelineDescription: - home: "บนไทม์ไลน์หลัก คุณสามารถดูโพสต์จากบัญชีที่ติดตามอยู่ได้" - local: "ไทม์ไลน์ท้องถิ่นช่วยให้เห็นโพสต์จากผู้ใช้ทั้งหมดบนเซิร์ฟเวอร์นี้" - social: "ไทม์ไลน์โซเชียลจะแสดงโพสต์จากทั้งไทม์ไลน์หลักและไทม์ไลน์ท้องถิ่น" - global: "ในไทม์ไลน์ทั่วโลก คุณสามารถดูโน้ตจากเซิร์ฟเวอร์ที่เชื่อมต่อทั้งหมดได้" _serverRules: description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ" _serverSettings: - iconUrl: "URL ไอคอน" - appIconDescription: "ระบุไอคอนที่จะใช้เมื่อ {host} แสดงเป็นแอป" - appIconUsageExample: "ตัวอย่างเช่น เมื่อถูกเพิ่มเป็น PWA หรือบุ๊กมาร์กบนหน้าจอหลักในสมาร์ทโฟน" - appIconStyleRecommendation: "เนื่องจากอาจถูกครอบตัดเป็นสี่เหลี่ยมหรือวงกลม จึงแนะนำให้ใช้ภาพที่เผื่อพื้นที่รอบๆ ตัวโลโก้ไอคอนไว้" + iconUrl: "ไอคอน URL" + appIconUsageExample: "E.g. เป็น PWA หรือเมื่อแสดงผลเป็นบุ๊กมาร์กหน้าจอหลักบนโทรศัพท์" appIconResolutionMustBe: "ความละเอียดขั้นต่ำไว้คือ {resolution}." - manifestJsonOverride: "เขียนทับ manifest.json" + manifestJsonOverride: "manifest.json โอเวอร์ลาย" shortName: "ชื่อย่อ" - shortNameDescription: "ตัวย่อหรือชื่อทั่วไปที่สามารถแสดงแทนชื่ออย่างเป็นทางการแบบยาวของเซิร์ฟเวอร์" - fanoutTimelineDescription: "เพิ่มประสิทธิภาพการดึงข้อมูลไทม์ไลน์อย่างมาก และลดภาระในฐานข้อมูลเมื่อเปิดใช้งาน ในทางกลับกัน การใช้หน่วยความจำของ Redis จะเพิ่มขึ้น ลองปิดการใช้งานนี้ในกรณีที่หน่วยความจำเซิร์ฟเวอร์เหลือน้อยหรือเซิร์ฟเวอร์ไม่เสถียร" - fanoutTimelineDbFallback: "ฟอลแบ๊กกลับฐานข้อมูล" - fanoutTimelineDbFallbackDescription: "เมื่อเปิดใช้งาน หากไม่ได้แคชไทม์ไลน์ ไทม์ไลน์จะฟอลแบ๊กไปยังฐานข้อมูลสำหรับการ query เพิ่มเติม การปิดใช้งานจะช่วยลดภาระของเซิร์ฟเวอร์ด้วยการกำจัดกระบวนฟอลแบ๊ก แต่มันก็จะจำกัดช่วงเวลาไทม์ไลน์ที่สามารถดึงข้อมูลได้" - inquiryUrl: "URL สำหรับการติดต่อสอบถาม" - inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์" _accountMigration: - moveFrom: "ย้ายจากบัญชีอื่นมาที่บัญชีนี้" + moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง" moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น" - moveFromLabel: "บัญชีที่จะย้ายจาก #{n}" - moveFromDescription: "หากต้องการโอนข้อมูลจากบัญชีอื่นมายังบัญชีนี้ จำเป็นต้องสร้างบัญชีนามแฝง (alias) ไว้ที่นี่ด้วย\nกรุณากรอกบัญชีเดิมในรูปแบบ: @username@server.example.com\nหากต้องการลบ alias, ให้เว้นว่างไว้แล้วบันทึก (ไม่แนะนำ)" - moveTo: "ย้ายบัญชีนี้ไปยังบัญชีใหม่" + moveFromLabel: "บัญชีที่จะย้ายจาก:" + moveFromDescription: "ถ้าหากคุณต้องการโอนข้อมูล คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี หลังจากนั้นป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com" + moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง" moveToLabel: "บัญชีที่จะย้ายไปที่:" moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้" - moveAccountDescription: "การดำเนินการนี้จะย้ายบัญชีของคุณไปยังบัญชีอื่น\n・ผู้ที่กำลังติดตามคุณจากบัญชีนี้จะถูกย้ายไปยังบัญชีใหม่โดยอัตโนมัติ\n・บัญชีนี้จะเลิกติดตามผู้ใช้ทั้งหมดที่กำลังติดตามอยู่\n・คุณจะไม่สามารถสร้างโน้ต ฯลฯ ในบัญชีนี้ได้\n\nแม้ว่าการย้ายผู้ที่ติดตามคุณจะเป็นไปโดยอัตโนมัติ แต่คุณต้องเตรียมขั้นตอนบางอย่างด้วยตนเอง เพื่อย้ายรายชื่อผู้ใช้ที่คุณกำลังติดตาม โดยดำเนินการส่งออกรายชื่อแล้วค่อยนำเข้ามาภายหลังในเมนูการตั้งค่าของบัญชีใหม่ ใช้ขั้นตอนเดียวกันนี้ใช้รายชื่อผู้ใช้ที่ถูกปิดเสียงและถูกบล็อก\n\n(คำอธิบายนี้ใช้กับ CherryPick v13.12.0 ขึ้นไป, ซอฟต์แวร์ ActivityPub อื่นๆ เช่น Mastodon อาจทำงานแตกต่างออกไป)" - moveAccountHowTo: "การย้ายบัญชีจะเริ่มต้นโดยการสร้างบัญชีนามแฝง (alias) ของบัญชีนี้ ณ บัญชีที่เป็นปลายทาง หลังจากสร้างนามแฝงแล้ว ให้ป้อนบัญชีปลายทางในรูปแบบดังนี้: @username@server.example.com" + moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com" + moveAccountHowTo: "หากต้องการย้ายข้อมูลก่อนอื่นให้สร้างชื่อแทนสำหรับบัญชีนี้ ในบัญชีที่จะต้องการย้ายไป\nหลังจากที่คุณสร้างนามแฝงนั้นแล้ว ให้ป้อนบัญชีที่ต้องการจะย้ายไปในรูปแบบดังต่อไปนี้: @username@server.example.com" startMigration: "โอนย้าย" migrationConfirm: "ยืนยันการย้ายข้อมูลบัญชีนี้ไปที่ {account} เมื่อเริ่มแล้วจะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี" - movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถยกเลิกการโอนย้ายได้" - postMigrationNote: "บัญชีนี้จะดำเนินการยกเลิกการติดตามทั้งหมดหลังจากการย้ายข้อมูลไปแล้ว 24 ชั่วโมง จำนวนกำลังติดตามและจำนวนผู้ติดตามของบัญชีนี้จะเป็น 0 และเพื่อหลีกเลี่ยงไม่ให้ผู้ติดตามคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามฯได้ การยกเลิกการติดตามจะไม่กระทบกับผู้ติดตามคุณ ดังนั้นผู้ติดตามคุณยังคงสามารถดูโพสต์ของบัญชีนี้ได้" - movedTo: "บัญชีที่จะย้ายไป:" + movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถย้อนกลับโอนย้ายข้อมูลได้" + postMigrationNote: "บัญชีนี้จะถูกเลิกติดตามบัญชีทั้งหมดที่กำลังติดตามภายใน 24 ชั่วโมงหลังจากการย้ายข้อมูลนั้นเสร็จสิ้น ทั้งจำนวนผู้ติดตามและผู้ติดตามนั้นจะกลายเป็นศูนย์ เพื่อหลีกเลี่ยงป้องกันไม่ให้ผู้ติดตามของคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามของบัญชีนี้ได้ แต่อย่างไรก็ตามแล้วพวกเขาจะยังคงติดตามบัญชีนี้ต่อไป" + movedTo: "บัญชีที่จะย้ายไปที่:" _achievements: earnedAt: "ได้รับเมื่อ" _types: _notes1: title: "just setting up my crpk" - description: "โพสต์โน้ตเป็นครั้งแรก" + description: "โพสต์โน้ตแรกของคุณ" flavor: "ขอให้มีช่วงเวลาที่ดีกับ CherryPick นะคะ!" _notes10: - title: "โน้ตไม่กี่ชิ้น" + title: "โน้ตบางอย่าง" description: "โพสต์ 10 โน้ต" _notes100: - title: "โน้ตเยอะอยู่" + title: "โน้ตจำนวนมาก" description: "โพสต์ 100 โน้ต" _notes500: - title: "จมคากองโน้ต" + title: "ครอบคลุมในโน้ต" description: "โพสต์ 500 โน้ต" _notes1000: title: "ภูเขาแห่งโน้ต" description: "โพสต์ 1,000 โน้ต" _notes5000: - title: "โน้ตล้นไปแล้ว" + title: "โน้ตล้น" description: "โพสต์ 5,000 โน้ต" _notes10000: title: "ซุปเปอร์โน้ต" description: "โพสต์ 10,000 โน้ต" _notes20000: - title: "ต้ อ ง ก า ร โ น้ ต เ พิ่ ม อี ก !" + title: "ต้องการ... เพิ่มเติม... โน้ต..." description: "โพสต์ 20,000 โน้ต" _notes30000: title: "โน้ต โน้ต โน้ต!" description: "โพสต์ 30,000 โน้ต" _notes40000: - title: "โรงงานผลิตโน้ต" + title: "โน้ตโรงงาน" description: "โพสต์ 40,000 โน้ต" _notes50000: title: "ดาวเคราะห์แห่งโน้ต" @@ -1469,26 +1274,26 @@ _achievements: title: "โน้ตควอซาร์" description: "โพสต์ 60,000 โน้ต" _notes70000: - title: "หลุม-โน้ต-ดำ" + title: "โน้ตหลุมดำ" description: "โพสต์ 70,000 โน้ต" _notes80000: - title: "ดาราจักรโน้ต" + title: "โน้ต กาแล็กซี่" description: "โพสต์ 80,000 โน้ต" _notes90000: - title: "จักรวาลโน้ต" + title: "โน้ต จักรวาล" description: "โพสต์ 90,000 โน้ต" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" description: "โพสต์ 100,000 โน้ต" - flavor: "มีเรื่องจะเขียนมากขนาดนั้นเลยเหรอนั่น?" + flavor: "นายแน่ใจล่ะก็ มีอะไรพูดมาได้นะ" _login3: title: "มือใหม่ I" description: "เข้าสู่ระบบเป็นเวลารวม 3 วัน" - flavor: "ตั้งแต่วันนี้เป็นต้นไป ฉันคือมิสคิสต์" + flavor: "เริ่มตั้งแต่วันนี้ เรียกฉันว่ามิสคิสต์" _login7: title: "มือใหม่ II" description: "เข้าสู่ระบบเป็นเวลารวม 7 วัน" - flavor: "ชินกับมันแล้วหรือยัง?" + flavor: "รู้สึกเหมือนคุณได้แขวนของสิ่งต่างๆ หรือยังคะ?" _login15: title: "มือใหม่ III" description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน" @@ -1501,7 +1306,7 @@ _achievements: _login100: title: "มิสคิสท์ III" description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน" - flavor: "มิสคิสต์หัวรุนแรง" + flavor: "ความรุนแรง Misskist" _login200: title: "ลูกค้าประจำ I" description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน" @@ -1514,7 +1319,7 @@ _achievements: _login500: title: "ผู้เชี่ยวชาญ I" description: "เข้าสู่ระบบเป็นเวลารวม 500 วัน" - flavor: "ทุกท่าน ผมชอบโน้ต (กล่าวโดย เดอะ เ_เ_อร์)" + flavor: "เพื่อนของผมนะมักจะกล่าวว่าผมนะชอบจดโน้ต" _login600: title: "ผู้เชี่ยวชาญ II" description: "เข้าสู่ระบบเป็นเวลารวม 600 วัน" @@ -1532,24 +1337,24 @@ _achievements: description: "เข้าสู่ระบบเป็นเวลารวม 1,000 วัน" flavor: "ขอบคุณที่ใช้ CherryPick นะ !" _noteClipped1: - title: "อดไม่ได้ที่จะต้องคลิปมันเอาไว้" - description: "คลิปโน้ตเป็นครั้งแรก" + title: "จะต้อง... คลิป..." + description: "คลิปโน้ตตัวแรกของคุณ" _noteFavorited1: title: "สตาร์เกเซอร์" - description: "ใส่โน้ตเป็นรายการโปรดเป็นครั้งแรก" + description: "ชื่นชอบโน้ตแรกของคุณ" _myNoteFavorited1: title: "แสวงหาดวงดาว" - description: "โน้ตตัวเองถูกคนอื่นเพิ่มลงรายการโปรดของเขา" + description: "มีคนอื่นๆที่ชื่นชอบหนึ่งในโน้ตของคุณ" _profileFilled: - title: "เตรียมตัวอย่างดี" - description: "ตั้งค่าโปรไฟล์" + title: "เตรียมไว้อย่างดี" + description: "ตั้งค่าโปรไฟล์ของคุณ" _markedAsCat: title: "ฉันเป็นแมว" - description: "ตั้งค่าบัญชีเป็นแมวเมี้ยวเมี้ยว" - flavor: "แมวน้อยไร้ชื่อ" + description: "ทำเครื่องหมายบัญชีของคุณว่าเป็นแมว" + flavor: "ฉันจะให้ชื่อคุณภายหลังนะ" _following1: - title: "ก้าวแรกสู่...กดติดตาม" - description: "กดติดตามชาวบ้านครั้งแรก" + title: "กำลังติดตามผู้ใช้คนแรกของคุณ" + description: "ติดตามผู้ใช้" _following10: title: "ทำต่อไป... ทำต่อไป..." description: "ติดตาม 10 บัญชีผู้ใช้" @@ -1560,7 +1365,7 @@ _achievements: title: "เพื่อน 100 คน" description: "ติดตาม 100 บัญชี" _following300: - title: "มีเพื่อนมากเกินไปละ" + title: "เพื่อนโอเวอร์โหลด" description: "ติดตาม 300 บัญชี" _followers1: title: "ผู้ติดตามคนแรก" @@ -1587,12 +1392,12 @@ _achievements: title: "นักสะสมความสำเร็จ" description: "ได้รับความสำเร็จ 30 ครั้ง" _viewAchievements3min: - title: "ชอบบรรลุความสําเร็จ" - description: "มองดูรายการความสำเร็จเป็นเวลานานกว่า 3 นาที" - _iLoveCherryPick: + title: "ชอบบรรลุผลสําเร็จ" + description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที" + _iLoveMisskey: title: "ฉันรัก CherryPick" - description: "โพสต์ “I ❤ #CherryPick”" - flavor: "ขอบคุณพระคุณเป็นอย่างสูงที่ท่านใช้ CherryPick นะคะ ! by ทีมผู้พัฒนา" + description: "โพสต์ \"I ❤ #CherryPick\"" + flavor: "ขอบคุณที่ใช้ CherryPick! by ทีมผู้พัฒนา" _foundTreasure: title: "ล่าสมบัติ" description: "คุณพบสมบัติที่ซ่อนอยู่" @@ -1600,28 +1405,28 @@ _achievements: title: "พักผ่อนสักหน่อย" description: "ใช้เวลา 30 นาทีบน CherryPick" _client60min: - title: "CherryPick ต้องไม่มีสิ่งใด “Miss”" + title: "ไม่มี \"Miss\" ใน CherryPick " description: "เปิด CherryPick ค้างไว้แล้วอย่างน้อย 60 นาที" _noteDeletedWithin1min: title: "ไม่เป็นไร" description: "ลบโน้ตภายในหนึ่งนาทีหลังจากที่โพสต์" _postedAtLateNight: - title: "ออกหากินยามดึกดื่น" + title: "กลางคืน" description: "โพสต์โน้ตตอนดึกๆ" flavor: "ได้เวลาเข้านอนแล้วนะ" _postedAt0min0sec: - title: "นาฬิกาเทียบเวลา" - description: "โพสต์โน้ตเมื่อเวลา 00:00 น." - flavor: "โป๊ะ โป๊ะ โป๊ะ ปิ้งงงงง" + title: "นาฬิกาพูดได้" + description: "โพสต์บนโน้ตเมื่อเวลา 00:00 น." + flavor: "คลิก คลิก คลิก แกล๊งๆ" _selfQuote: title: "อ้างอิงตนเอง" - description: "อ้างอิงโน้ตตัวเอง" + description: "อ้างโน้ตย่อของคุณเอง" _htl20npm: title: "ไทม์ไลน์ไหล" - description: "มีการทำความเร็วของไทม์ไลน์หลักเกิน 20 npm (โน้ตต่อนาที)" + description: "มีการทำความเร็วของไทม์ไลน์ที่บ้านของคุณเกิน 20 npm (โน้ตต่อนาที)" _viewInstanceChart: title: "วิเคราะห์" - description: "ดูแผนภูมิของเซิร์ฟเวอร์" + description: "ดูแผนภูมิอินสแตนซ์ของคุณ" _outputHelloWorldOnScratchpad: title: "หวัดดีชาวโลก!" description: "เอาพุต \"hello world\" ใน Scratchpad" @@ -1635,26 +1440,26 @@ _achievements: title: "คุณอ่านมันจริงๆหรือเปล่า?" description: "มีการโต้ตอบกับโน้ตที่มีความยาวมากกว่า 100 ตัวอักษรภายใน 3 วินาทีหลังจากที่โพสต์" _clickedClickHere: - title: "คลิกที่นี่" + title: "คลิ๊กที่นี่" description: "คุณได้คลิกที่นี่" _justPlainLucky: title: "แค่ลัคกี้ธรรมดา" description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที" _setNameToSyuilo: - title: "คอมเพล็กซ์ของพระเจ้า" - description: "ตั้งชื่อเป็น “syuilo”" + title: "พระเจ้าคอมเพล็กซ์" + description: "ตั้งชื่อของคุณเป็น \"syuilo\"" _setNameToNoriDev: - title: "คอมเพล็กซ์ของพระเจ้า (CherryPick)" - description: "ตั้งชื่อเป็น “noridev”" + title: "พระเจ้าคอมเพล็กซ์ (CherryPick)" + description: "ตั้งชื่อของคุณเป็น \"noridev\"" _passedSinceAccountCreated1: title: "ครบรอบหนึ่งปี" - description: "ผ่านไป 1 ปีนับตั้งแต่สร้างบัญชี" + description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" _passedSinceAccountCreated2: title: "ครบรอบสองปี" - description: "ผ่านไป 2 ปีนับตั้งแต่สร้างบัญชี" + description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" _passedSinceAccountCreated3: title: "ครบรอบสามปี" - description: "ผ่านไป 3 ปีนับตั้งแต่สร้างบัญชี" + description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" _loggedInOnBirthday: title: "สุขสันต์วันเกิด" description: "เข้าสู่ระบบในวันเกิดของคุณ" @@ -1665,7 +1470,7 @@ _achievements: _cookieClicked: title: "เกมที่คุณคลิกที่คุกกี้" description: "คลิกคุกกี้" - flavor: "ใช่หรอ? แน่ใจว่าซอฟต์แวร์ทำงานถูกต้องนะ?" + flavor: "เดี๋ยวก่อนนะ คุณอยู่ในเว็บไซต์ที่ถูกต้องแน่อย่างงั้นเหรอ?" _brainDiver: title: "Brain Diver" description: "โพสต์ลิงก์ไปยัง Brain Diver" @@ -1673,67 +1478,53 @@ _achievements: _smashTestNotificationButton: title: "ทดสอบโอเวอร์โฟลว์" description: "ทดสอบการแจ้งเตือนทริกเกอร์ซ้ำๆ ภายในระยะเวลาอันสั้นๆ" - _tutorialCompleted: - title: "ใบรับรองการสำเร็จหลักสูตร CherryPick มือใหม่" - description: "เสร็จสิ้นการสอนแล้ว" - _bubbleGameExplodingHead: - title: "🤯" - description: "สร้างวัตถุที่ใหญ่ที่สุดในเกมบับเบิ้ล" - _bubbleGameDoubleExplodingHead: - title: "ดับเบิ้ล" - description: "สร้างวัตถุที่ใหญ่ที่สุดในเกมบับเบิ้ลสองชิ้นในเวลาเดียวกัน" - flavor: "ปิ่นโตขนาดนี้ น่าจะเพิ่ม 🤯 🤯 เข้าไปนิดหน่อย" _role: new: "บทบาทใหม่" edit: "แก้ไขบทบาท" name: "ชื่อบทบาท" description: "คำอธิบายบทบาท" permission: "สิทธิ์ตามบทบาท" - descriptionOfPermission: "ผู้ควบคุม สามารถดำเนินการดูแลขั้นพื้นฐานได้\nผู้ดูแลระบบ สามารถเปลี่ยนการตั้งค่าทั้งหมดของเซิร์ฟเวอร์ได้" + descriptionOfPermission: "ผู้ดูแลกลั่นกรองเนื้อหา สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\nผู้ดูแลระบบ สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ" assignTarget: "มอบหมาย" - descriptionOfAssignTarget: "แบบปรับเอง เพิ่มถอนบทบาทนี้แก่ผู้ใช้ด้วยตัวเอง\nแบบมีเงื่อนไข เพิ่มถอนบทบาทนี้แก่ผู้ใช้โดยอัตโนมัติหากเข้าเงื่อนไขใดต่อไปนี้" + descriptionOfAssignTarget: "แมนนวล เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\nเงื่อนไข เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง" manual: "ปรับเอง" - manualRoles: "บทบาทแบบทำมือ" conditional: "มีเงื่อนไข" - conditionalRoles: "บทบาทแบบมีเงื่อนไข" condition: "เงื่อนไข" isConditionalRole: "นี่คือบทบาทที่มีเงื่อนไข" - isPublic: "ทำให้บทบาทเปิดเผยต่อสาธารณะ" - descriptionOfIsPublic: "บทบาทจะปรากฏบนโปรไฟล์ของผู้ใช้และเปิดเผยต่อสาธารณะ (ทุกคนสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)" + isPublic: "บทบาทสาธารณะ" + descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย" options: "ตัวเลือกบทบาท" policies: "นโยบาย" - baseRole: "แม่แบบบทบาท" - useBaseValue: "ใช้ตามแม่แบบบทบาท" + baseRole: "บทบาทพื้นฐาน" + useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" - iconUrl: "URL ไอคอน" + iconUrl: "ไอคอน URL" asBadge: "แสดงเป็นตรา" - descriptionOfAsBadge: "เมื่อเปิดใช้งาน ไอคอนบทบาทจะปรากฏถัดจากชื่อผู้ใช้" - isExplorable: "ค้นหาผู้ใช้ได้ง่ายขึ้นโดยดูจากบทบาท" - descriptionOfIsExplorable: "เมื่อเปิดใช้งาน ไทมไลน์บทบาทนี้และสมาชิกที่มีบทบาทนี้จะเปิดเผยเป็นสาธารณะ" - displayOrder: "ลำดับการแสดงผล" - descriptionOfDisplayOrder: "เลขที่สูงกว่าจะแสดงบน UI ก่อน" - canEditMembersByModerator: "อนุญาตให้ผู้ควบคุมแก้ไขสมาชิก" - descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ นอกเหนือจากผู้ควบคุมและผู้ดูแลระบบแล้ว จะสามารถเพิ่มถอนบทบาทนี้แก่ผู้ใช้ได้ แต่เมื่อปิดใช้ จะมีเฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถดำเนินการได้" + descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน" + isExplorable: "บทบาทไทม์ไลน์เป็นแบบสาธารณะ" + descriptionOfIsExplorable: "ไทม์ไลน์ของบทบาทนี้จะสามารถเข้าถึงได้แบบสาธารณะถ้าหากเปิดใช้งาน เส้นเวลาของบทบาทนั้นจะไม่ถูกเปิดเผยต่อสาธารณะ ถึงแม้ว่าจะไม่เปิดเผยต่อสาธารณะแม้แต่ว่า...จะตั้งค่าไว้ยังไงก็ตาม" + displayOrder: "ตำแหน่ง" + descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ" + canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" + descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" priority: "ลำดับความสำคัญ" _priority: low: "ต่ำ" middle: "ปานกลาง" high: "สูง" _options: - gtlAvailable: "สามารถดูไทม์ไลน์ทั่วโลกได้" - ltlAvailable: "สามารถดูไทม์ไลน์ท้องถิ่นได้" - canPublicNote: "สามารถโพสต์แบบสาธารณะ" + gtlAvailable: "การดูไทม์ไลน์ทั่วโลก" + ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" + canPublicNote: "สามารถส่งโน้ตสาธารณะ" canEditNote: "กำลังแก้ไขโน้ต" - mentionMax: "จำนวนการกล่าวถึงสูงสุดต่อโน้ต" - canInvite: "สร้างรหัสเชิญเข้าเซิร์ฟเวอร์" + canInvite: "สร้างรหัสเชิญอินสแตนซ์" inviteLimit: "จำกัดการเชิญ" - inviteLimitCycle: "คูลดาวน์ในการเชิญ" + inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์" inviteExpirationTime: "วันหมดอายุของรหัสการเชิญ" - canManageCustomEmojis: "จัดการเอโมจิที่กำหนดเอง" + canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" canManageAvatarDecorations: "จัดการตกแต่งอวตาร" driveCapacity: "ความจุของไดรฟ์" alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ" - canUpdateBioMedia: "อนุญาตให้ปรับปรุงไอคอนและแบนเนอร์" pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้" antennaMax: "จำนวนสูงสุดของเสาอากาศ" wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ" @@ -1742,21 +1533,14 @@ _role: noteEachClipsMax: "จำนวนโน้ตสูงสุดภายในคลิป" userListMax: "จำนวนรายชื่อผู้ใช้สูงสุด" userEachUserListsMax: "จำนวนผู้ใช้สูงสุดภายในรายการผู้ใช้" - rateLimitFactor: "อัตราการจำกัด" - descriptionOfRateLimitFactor: "ยิ่งตัวเลขน้อยก็ยิ่งจำกัดน้อย ยิ่งมากก็ยิ่งเข้มงวดมากขึ้น" + rateLimitFactor: "ขีดจำกัดอัตรา" + descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า" canHideAds: "ซ่อนโฆษณา" canSearchNotes: "การใช้การค้นหาโน้ต" canUseTranslator: "การใช้งานแปล" - avatarDecorationLimit: "จำนวนการตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้" _condition: - roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ" - isLocal: "ผู้ใช้ท้องถิ่น" + isLocal: "ผู้ใช้ภายใน" isRemote: "ผู้ใช้ระยะไกล" - isCat: "ผู้ใช้ที่เป็นแมว" - isBot: "ผู้ใช้ที่เป็นบอต" - isSuspended: "ผู้ใช้ที่ถูกระงับ" - isLocked: "ผู้ใช้บัญชีไม่เปิดเผยสาธารณะ" - isExplorable: "ผู้ใช้ที่เปิดใช้งาน “ทำให้บัญชีของฉันค้นหาได้ง่ายขึ้น”" createdLessThan: "สร้างน้อยกว่า" createdMoreThan: "สร้างมากกว่า" followersLessThanOrEq: "จำนวนผู้ติดตามน้อยกว่าหรือเท่ากับ\n" @@ -1769,10 +1553,10 @@ _role: or: "หรือ" not: "ไม่" _sensitiveMediaDetection: - description: "ใช้ Machine Learning เพื่อตรวจจับสื่อที่มีเนื้อหาละเอียดอ่อนโดยอัตโนมัติและใช้เพื่อการกลั่นกรอง ภาระของเซิร์ฟเวอร์จะเพิ่มขึ้นเล็กน้อย" - sensitivity: "ความไวในการตรวจจับ" - sensitivityDescription: "เมื่อความไวต่ำ Misdetection (ผลบวกลวง) จะลดลง, เมื่อความไวสูง Missed detection (ผลลบลวง) จะลดลง" - setSensitiveFlagAutomatically: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน" + description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" + sensitivity: "การตรวจจับความไว" + sensitivityDescription: "การลดความไวนั้นจะนำไปสู่การตรวจจับที่ผิดพลาดน้อยลง (ผลบวกที่ผิดพลาด) แต่ในขณะที่การเพิ่มนั้นจะนำไปสู่การตรวจหาที่พลาดน้อยลง (ผลลบเท็จ)" + setSensitiveFlagAutomatically: "ทำเครื่องหมายว่าเป็น NSFW" setSensitiveFlagAutomaticallyDescription: "ผลลัพธ์ของการตรวจจับภายในนั้นจะยังคงอยู่ ถึงแม้ว่าจะปิดตัวเลือกนี้" analyzeVideos: "เปิดใช้งานวิเคราะห์ของวิดีโอ" analyzeVideosDescription: "การวิเคราะห์วิดีโอนอกเหนือจากรูปภาพนั้น การทำสิ่งนี้จะทำให้เพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" @@ -1782,19 +1566,18 @@ _emailUnavailable: disposable: "ไม่สามารถใช้อีเมลชั่วคราวได้" mx: "เซิร์ฟเวอร์อีเมลนี้ไม่ถูกต้อง" smtp: "เซิร์ฟเวอร์อีเมลนี้ไม่มีการตอบสนอง" - banned: "คุณไม่สามารถลงทะเบียนด้วยที่อยู่อีเมลนี้ได้" _ffVisibility: - public: "สาธารณะ" + public: "เผยแพร่" followers: "ปรากฏให้แก่ผู้ติดตามเท่านั้น" private: "ส่วนตัว" _signup: - almostThere: "เกือบจะเสร็จแล้ว" - emailAddressInfo: "กรุณากรอกที่อยู่อีเมลที่คุณใช้ ที่อยู่อีเมลของคุณจะไม่ถูกเผยแพร่สู่สาธารณชน" - emailSent: "อีเมลยืนยันได้ถูกส่งไปยังที่อยู่อีเมลที่คุณป้อน ({email}) แล้ว กรุณาติดตามลิงก์ในอีเมลเพื่อสร้างบัญชีให้เสร็จสมบูรณ์ ลิงก์ที่ให้ไว้จะหมดอายุใน 30 นาที" + almostThere: "เกือบจะมี" + emailAddressInfo: "โปรดกรอกอีเมลของคุณ มันจะไม่เปิดเผยต่อสาธารณะ" + emailSent: "เราได้ส่งอีเมลยืนยันไปยังที่อยู่อีเมลของคุณแล้วนะ ({email}) โปรดคลิกลิงก์ที่รวมไว้เพื่อสร้างบัญชีให้เสร็จสิ้น" _accountDelete: accountDelete: "ลบบัญชีผู้ใช้" mayTakeTime: "เนื่องจากการลบบัญชีนี้จะเป็นกระบวนการที่ต้องใช้ทรัพยากรมาก จึงอาจจะต้องใช้เวลาสักครู่ถึงจะเสร็จสมบูรณ์ ทั้งนี้ขึ้นอยู่กับจำนวนเนื้อหาที่คุณสร้างและจำนวนไฟล์ที่คุณอัปโหลดนะ" - sendEmail: "เมื่อการลบบัญชีเสร็จสิ้น การแจ้งเตือนจะถูกส่งไปยังที่อยู่อีเมลที่ลงทะเบียนไว้" + sendEmail: "เมื่อการลบบัญชีนี้เสร็จสิ้น เราอาจจะส่งอีเมลไปยังที่อยู่อีเมลของคุณที่เคยลงทะเบียนไว้กับบัญชีนี้นะ" requestAccountDelete: "ร้องขอให้ลบบัญชี" started: "การลบได้เริ่มต้นขึ้น" inProgress: "ปัจจุบันกำลังดำเนินการลบอยู่" @@ -1804,18 +1587,16 @@ _ad: hide: "ไม่ต้องแสดง" timezoneinfo: "วันในสัปดาห์นี้จะถูกกำหนดจากโซนเวลาของเซิร์ฟเวอร์" adsSettings: "ตั้งค่าการโฆษณา" - notesPerOneAd: "อัปเดตช่วงเวลาตำแหน่งโฆษณาแบบเรียลไทม์ (จำนวนโน้ตต่อโฆษณา)" setZeroToDisable: "ตั้งค่านี้ให้เป็น 0 เพื่อปิดใช้งานโฆษณาอัปเดตแบบเรียลไทม์" - adsTooClose: "เนื่องจากช่วงเวลาการแสดงโฆษณาสั้นมาก ประสบการณ์ผู้ใช้จึงอาจลดลงอย่างมาก" _forgotPassword: enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ" - ifNoEmail: "หากลงทะเบียนแบบไม่ใช้อีเมล โปรดติดต่อผู้ดูแลระบบ" - contactAdmin: "เนื่องจากเซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล หากต้องการรีเซ็ตรหัสผ่าน กรุณาติดต่อผู้ดูแลระบบ" + ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ" + contactAdmin: "อินสแตนซ์นี้ไม่รองรับการใช้งานที่อยู่อีเมลนี้ กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์เพื่อรีเซ็ตรหัสผ่านของคุณแทน" _gallery: my: "แกลลอรี่ของฉัน" liked: "โพสต์ที่ถูกใจ" - like: "ถูกใจ!" - unlike: "เลิกถูกใจ" + like: "ชื่นชอบ" + unlike: "ลบไลค์" _email: _follow: title: "ได้ติดตามคุณ" @@ -1826,25 +1607,24 @@ _plugin: installWarn: "กรุณาอย่าติดตั้งปลั๊กอินที่ไม่น่าเชื่อถือนะคะ" manage: "จัดการปลั๊กอิน" viewSource: "ดูต้นฉบับ" - viewLog: "แสดงปูม" _preferencesBackups: - list: "การตั้งค่าที่สำรองไว้" - saveNew: "บันทึกการตั้งค่าสำรองใหม่" + list: "สร้างการสำรองข้อมูล" + saveNew: "บันทึกใหม่" loadFile: "โหลดจากไฟล์" apply: "นำไปใช้กับอุปกรณ์นี้" save: "บันทึก" - inputName: "กรุณาป้อนชื่อการตั้งค่าสำรองนี้" + inputName: "กรุณาป้อนชื่อสำหรับข้อมูลสำรองนี้" cannotSave: "การบันทึกล้มเหลว" - nameAlreadyExists: "มีการตั้งค่าสำรองชื่อ “{name}” อยู่แล้ว กรุณาป้อนชื่ออื่น" - applyConfirm: "ต้องการใช้การตั้งค่าสำรอง “{name}” กับอุปกรณ์นี้ใช่ไหม? การตั้งค่าที่มีอยู่บนอุปกรณ์นี้จะถูกเขียนทับ" - saveConfirm: "บันทึกการตั้งค่าสำรองเป็น {name} ใช่ไหม?" - deleteConfirm: "ต้องการลบ {name} ใช่ไหม?" - renameConfirm: "ต้องการเปลี่ยนชื่อจาก “{old}” เป็น “{new}” ใช่ไหม?" - noBackups: "ไม่มีการตั้งค่าสำรอง สามารถบันทึกการตั้งค่าไคลเอนต์ปัจจุบันไปยังเซิร์ฟเวอร์ด้วย “บันทึกการตั้งค่าสำรองใหม่”" + nameAlreadyExists: "มีข้อมูลสำรองชื่อ \"{name}\" นี้อยู่แล้ว กรุณาป้อนชื่ออื่นนะ" + applyConfirm: "คุณต้องการใช้ข้อมูลสำรอง \"{name}\" กับอุปกรณ์นี้อย่างงั้นจริงหรอ การตั้งค่าที่มีอยู่ของอุปกรณ์นี้จะถูกเขียนทับนะ" + saveConfirm: "บันทึกข้อมูลสำรองเป็น {name} มั้ย?" + deleteConfirm: "ลบข้อมูลสำรอง {name} มั้ย?" + renameConfirm: "เปลี่ยนชื่อข้อมูลสำรองนี้จาก \"{old}\" เป็น \"{new}\" หรือป่าว" + noBackups: "ไม่มีข้อมูลสำรองนะ คุณสามารถสำรองข้อมูลการตั้งค่าไคลเอนต์ของคุณบนเซิร์ฟเวอร์นี้โดยใช้ \"สร้างการสำรองข้อมูลใหม่\"ได้นะ" createdAt: "สร้างเมื่อ: {date} {time}" updatedAt: "อัปเดตเมื่อ: {date} {time}" cannotLoad: "การโหลดล้มเหลว" - invalidFile: "รูปแบบไฟล์ไม่ถูกต้อง" + invalidFile: "รูปแบบไฟล์ไม่ถูกต้องนะ" _registry: scope: "สโคป" key: "คีย์" @@ -1856,16 +1636,13 @@ _aboutMisskey: contributors: "ผู้สนับสนุนหลัก" allContributors: "ผู้มีส่วนร่วมทั้งหมด" source: "ซอร์สโค้ด" - original: "ต้นฉบับ" - thisIsModifiedVersion: "{name} ใช้ CherryPick เวอร์ชันดัดแปลง" translation: "แปลภาษา Misskey" donate: "บริจาคให้กับ Misskey" - morePatrons: "และอีกหลายท่านที่ไม่ได้เอ่ยนาม ขอบคุณที่ร่วมช่วยเหลือตลอดมานะคะ 🥰" - patrons: "ผู้อุปถัมภ์" - projectMembers: "สมาชิกในโครงการ" + morePatrons: " ขอบคุณทุกท่านที่ร่วมกันช่วยเหลือตลอดมานะคะ 🥰" + patrons: "สมาชิกพันธมิตร" _displayOfSensitiveMedia: - respect: "ซ่อนสื่อที่มีเนื้อหาละเอียดอ่อน" - ignore: "แสดงสื่อที่มีเนื้อหาละเอียดอ่อน" + respect: "ซ่อนสื่อทำเครื่องหมายบอกว่าละเอียดอ่อน" + ignore: "แสดงผลสื่อทำเครื่องหมายบอกว่าละเอียดอ่อน" force: "ซ่อนสื่อทั้งหมด" _mfm: cheatSheet: "โค้ด MFM Cheat Sheet" @@ -1942,18 +1719,17 @@ _serverDisconnectedBehavior: dialog: "แสดงกล่องโต้ตอบคำเตือน" quiet: "แสดงคำเตือนที่ไม่เป็นการรบกวน" _channel: - create: "สร้างช่องใหม่" - edit: "แก้ไขช่อง" + create: "สร้างแชนแนลใหม่" + edit: "แก้ไขแชนแนล" setBanner: "เซตแบนเนอร์" removeBanner: "ลบแบนเนอร์" featured: "เทรนด์" owned: "เจ้าของ" following: "ติดตามแล้ว" usersCount: "{n} ผู้เข้าร่วม" - notesCount: "มี {n} โน้ต" + notesCount: "{n} โน้ต" nameAndDescription: "ชื่อและคำอธิบาย" nameOnly: "ชื่อเท่านั้น" - allowRenoteToExternal: "อนุญาตให้รีโน้ตและอ้างอิงนอกช่องได้" _menuDisplay: sideFull: "ด้านข้าง" sideIcon: "ด้านข้าง (ไอคอน)" @@ -1961,13 +1737,13 @@ _menuDisplay: hide: "ซ่อน" _wordMute: muteWords: "ปิดเสียงคำ" - muteWordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR" + muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ" muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป" _instanceMute: - instanceMuteDescription: "ปิดเสียง “โน้ต/รีโน้ต” ทั้งหมดจากเซิร์ฟเวอร์ที่ระบุไว้ รวมถึงโน้ตของผู้ใช้ที่ตอบกลับผู้ใช้จากเซิร์ฟเวอร์ที่ถูกปิดเสียง" + instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง" instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่" - title: "ซ่อนโน้ตจากเซิร์ฟเวอร์ที่มีระบุไว้" - heading: "เซิร์ฟเวอร์ที่ถูกปิดเสียง" + title: "ซ่อนโน้ตจากอินสแตนซ์ที่มีอยู่ในรายการ" + heading: "รายชื่ออินสแตนซ์ที่ถูกปิดเสียง" _theme: explore: "สำรวจธีม" install: "ติดตั้งธีม" @@ -1999,8 +1775,8 @@ _theme: importInfo: "ถ้าหากต้องการป้อนโค้ดที่นี่ คุณยังสามารถนำเข้าไปยังโปรแกรมแก้ไขธีมได้" deleteConstantConfirm: "คุณต้องการลบค่าคงที่ {const} หรือป่าว?" keys: - accent: "สีหลัก" - bg: "พื้นหลัง" + accent: "เน้น" + bg: "ภาพพื้นหลัง" fg: "ข้อความ" focus: "โฟกัส" indicator: "ตัวบ่งชี้" @@ -2036,24 +1812,17 @@ _theme: wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ" badge: "ตรา" messageBg: "พื้นหลังแชท" - accentDarken: "สีหลัก (มืด)" - accentLighten: "สีหลัก (สว่าง)" + accentDarken: "เน้น (มืด)" + accentLighten: "เน้น (สว่าง)" fgHighlighted: "ข้อความที่ไฮไลต์" _sfx: - note: "โน้ต" + note: "หมายเหตุ" noteMy: "โน้ตของตัวเอง" notification: "การเเจ้งเตือน" chat: "แชท" chatBg: "แชท (พื้นหลัง)" - reaction: "เมื่อเลือกรีแอคชั่น" -_soundSettings: - driveFile: "ใช้เสียงจากไดรฟ์" - driveFileWarn: "เลือกไฟล์ในไดรฟ์ของคุณ" - driveFileTypeWarn: "ไม่รองรับไฟล์นี้" - driveFileTypeWarnDescription: "กรุณาเลือกไฟล์เสียง" - driveFileDurationWarn: "เสียงยาวเกินไป" - driveFileDurationWarnDescription: "การใช้เสียงที่ยาว อาจรบกวนการใช้งาน CherryPick, ต้องการดำเนินการต่อใช่ไหม?" - driveFileError: "ไม่สามารถโหลดไฟล์เสียงได้ กรุณาเปลี่ยนแปลงการตั้งค่า" + antenna: "เสาอากาศ" + channel: "การแจ้งเตือนช่อง" _ago: future: "อนาคต" justNow: "เมื่อกี๊นี้" @@ -2065,14 +1834,6 @@ _ago: monthsAgo: "{n} เดือนที่แล้ว" yearsAgo: "{n} ปีที่ผ่านมา" invalid: "ไม่พบผลลัพธ์" -_timeIn: - seconds: "ใน {n} วินาที" - minutes: "ใน {n} นาที" - hours: "ใน {n} ชั่วโมง" - days: "ใน {n} วัน" - weeks: "ใน {n} สัปดาห์" - months: "ใน {n} เดือน" - years: "ใน {n} ปี" _time: second: "วินาที" minute: "นาที" @@ -2083,6 +1844,7 @@ _2fa: registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์" step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ" step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้" + step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้" step2Uri: "ป้อนใส่ URL ดังต่อไปนี้ถ้าหากคุณใช้โปรแกรมเดสก์ท็อป" step3Title: "ป้อนรหัสยืนยัน" step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า" @@ -2097,105 +1859,54 @@ _2fa: removeKey: "ลบคีย์ความปลอดภัยออก" removeKeyConfirm: "ลบข้อมูลสำรอง {name} มั้ย?" whyTOTPOnlyRenew: "ไม่สามารถลบแอปตัวรับรองความถูกต้องได้ตราบใดที่มีการลงทะเบียนคีย์ความปลอดภัยไว้แล้ว" - renewTOTP: "ตั้งค่าแอปยืนยันตัวตน" + renewTOTP: "กำหนดค่าแอพตัวตรวจสอบสิทธิ์ใหม่" renewTOTPConfirm: "วิธีการแบบนี้จะทําให้รหัสยืนยันจากแอพก่อนหน้าของคุณหยุดทํางานเลยนะ" renewTOTPOk: "ตั้งค่าคอนฟิกใหม่" renewTOTPCancel: "ไม่เป็นไร" - checkBackupCodesBeforeCloseThisWizard: "โปรดตรวจสอบรหัสแบ๊กอัปด้านล่างก่อนที่จะปิดวิซาร์ดนี้" - backupCodes: "รหัสแบ๊กอัป" - backupCodesDescription: "หากแอปยืนยันตัวตนของคุณไม่พร้อมใช้งาน คุณสามารถใช้รหัสสำรองด้านล่างเพื่อเข้าถึงบัญชีของคุณได้ อย่าลืมเก็บรหัสเหล่านี้ไว้ในที่ปลอดภัย แต่ละรหัสสามารถใช้ได้เพียงครั้งเดียวเท่านั้น" - backupCodeUsedWarning: "รหัสแบ๊กอัปถูกใช้งานแล้ว หากแอปพลิเคชันการยืนยันตัวตนไม่สามารถใช้งานได้ ให้รีบทำการตั้งค่าแอปฯใหม่โดยเร็วที่สุด" - backupCodesExhaustedWarning: "รหัสแบ๊กอัปทั้งหมดถูกใช้งานแล้ว หากยังไม่สามารถใช้แอปพลิเคชันการยืนยันตัวตนได้ก็จะไม่สามารถเข้าถึงบัญชีนี้ได้อีกต่อไป กรุณาลงทะเบียนแอปพลิเคชันการยืนยันตัวตนใหม่" - moreDetailedGuideHere: "คลิกที่นี่เพื่อดูคำแนะนำโดยละเอียด" + backupCodes: "รหัสสำรองข้อมูล" + backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีก" + backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้ว ถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะยังไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น" _permissions: - "read:account": "ดูข้อมูลบัญชี" - "write:account": "แก้ไขข้อมูลบัญชี" - "read:blocks": "ดูรายชื่อผู้ใช้ที่ถูกบล็อก" - "write:blocks": "แก้ไขรายชื่อผู้ใช้ที่ถูกบล็อก" - "read:drive": "เข้าถึงไดรฟ์" - "write:drive": "จัดการไดรฟ์" + "read:account": "ดูข้อมูลบัญชีของคุณ" + "write:account": "แก้ไขข้อมูลบัญชีของคุณ" + "read:blocks": "ดูรายชื่อผู้ใช้ที่ถูกบล็อกของคุณ" + "write:blocks": "แก้ไขรายชื่อผู้ใช้ที่ถูกบล็อกของคุณ" + "read:drive": "เข้าถึงไฟล์และโฟลเดอร์ในไดรฟ์ของคุณ" + "write:drive": "แก้ไขหรือลบไฟล์และโฟลเดอร์ในไดรฟ์ของคุณ" "read:favorites": "ดูรายการโปรด" "write:favorites": "แก้ไขรายการโปรด" "read:following": "ดูข้อมูลว่าใครที่คุณติดตาม" "write:following": "ติดตามหรือเลิกติดตามบัญชีอื่น" - "read:messaging": "ดูแชท" + "read:messaging": "ดูแชทของคุณ" "write:messaging": "เขียนหรือลบข้อความแชท" - "read:mutes": "ดูรายชื่อผู้ใช้ที่ถูกปิดเสียง" + "read:mutes": "ดูรายชื่อผู้ใช้ที่ปิดเสียงของคุณ" "write:mutes": "แก้ไขรายชื่อผู้ใช้ที่ถูกปิดเสียง" "write:notes": "เขียนหรือลบโน้ต" - "read:notifications": "ดูการแจ้งเตือน" - "write:notifications": "จัดการแจ้งเตือน" - "read:reactions": "ดูรีแอคชั่น" - "write:reactions": "แก้ไขรีแอคชั่น" + "read:notifications": "ดูการแจ้งเตือนของคุณ" + "write:notifications": "จัดการแจ้งเตือนของคุณ" + "read:reactions": "ดูปฏิกิริยาของคุณ" + "write:reactions": "แก้ไขปฏิกิริยาของคุณ" "write:votes": "โหวตบนสำรวจความคิดเห็น" - "read:pages": "ดูหน้าเพจ" - "write:pages": "แก้ไขหรือลบเพจ" - "read:page-likes": "ดูรายการเพจที่ถูกใจไว้" - "write:page-likes": "แก้ไขรายการเพจที่ถูกใจ" - "read:user-groups": "ดูกลุ่มผู้ใช้" - "write:user-groups": "แก้ไขหรือลบกลุ่มผู้ใช้" - "read:channels": "ดูช่อง" - "write:channels": "แก้ไขช่อง" + "read:pages": "ดูหน้า" + "write:pages": "แก้ไขหรือลบเพจของคุณ" + "read:page-likes": "ดูไลค์ของคุณบนเพจ" + "write:page-likes": "แก้ไขการถูกใจของคุณบนเพจ" + "read:user-groups": "ดูกลุ่มผู้ใช้ของคุณ" + "write:user-groups": "แก้ไขหรือลบกลุ่มผู้ใช้ของคุณ" + "read:channels": "ดูแชนแนลของคุณ" + "write:channels": "แก้ไขแชนแนลของคุณ" "read:gallery": "ดูแกลเลอรี่" - "write:gallery": "แก้ไขแกลเลอรี" - "read:gallery-likes": "ดูแกลเลอรีที่ถูกใจไว้" - "write:gallery-likes": "จัดการแกลเลอรีที่ถูกใจไว้" - "read:flash": "ดู Play" - "write:flash": "แก้ไข Play" - "read:flash-likes": "ดูรายการ play ที่ถูกใจไว้" - "write:flash-likes": "แก้ไขรายการ play ที่ถูกใจไว้" - "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": "ดูที่อยู่ IP ของผู้ใช้" - "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: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": "ดูข้อมูลเกี่ยวกับไดรฟ์ของผู้ใช้" - "read:admin:stream": "ใช้ Websocket API สำหรับผู้ดูแลระบบ" - "write:admin:ad": "จัดการโฆษณา" - "read:admin:ad": "ดูโฆษณา" - "write:invite-codes": "สร้างรหัสเชิญ" - "read:invite-codes": "รับรหัสเชิญ" - "write:clip-favorite": "จัดการคลิปที่ถูกใจ" - "read:clip-favorite": "ดูคลิปที่ถูกใจ" - "read:federation": "รับข้อมูลเกี่ยวกับสหพันธ์" - "write:report-abuse": "รายงานการละเมิด" + "write:gallery": "แก้ไขแกลเลอรี่ของคุณ" + "read:gallery-likes": "ดูรายการโพสต์ในแกลเลอรีที่ชอบของคุณ" + "write:gallery-likes": "แก้ไขรายการโพสต์ในแกลเลอรีที่ชอบของคุณ" + "read:flash": "วิว เพลย์" + "write:flash": "แก้ไขเพลย์" + "read:flash-likes": "ดูรายชื่อของไลค์ เพลย์" + "write:flash-likes": "แก้ไขรายชื่อของไลค์ เพลย์" _auth: shareAccessTitle: "การให้สิทธิ์แอปพลิเคชัน" shareAccess: "คุณต้องการอนุญาตให้ \"{name}\" เข้าถึงบัญชีนี้เลยมั้ย?" - shareAccessAsk: "ต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณใช่ไหม?" + shareAccessAsk: "คุณแน่ใจแล้วจริงๆหรอว่าต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณแน่ใจแล้วหรอ?" permission: "{name} ได้ขอสิทธิ์การเข้าถึงดังต่อไปนี้" permissionAsk: "แอปพลิเคชันนี้ขอสิทธิ์ดังต่อไปนี้" pleaseGoBack: "กรุณากลับไปที่แอปพลิเคชัน" @@ -2219,7 +1930,7 @@ _weekday: saturday: "วันเสาร์" _widgets: profile: "โปรไฟล์" - instanceInfo: "ข้อมูลเซิร์ฟเวอร์" + instanceInfo: "ข้อมูล อินสแตนซ์" memo: "โน้ตแปะ" notifications: "การเเจ้งเตือน" timeline: "ไทม์ไลน์" @@ -2232,22 +1943,21 @@ _widgets: photos: "รูปภาพ" digitalClock: "นาฬิกาดิจิตอล" unixClock: "นาฬิกา UNIX" - federation: "สหพันธ์" - instanceCloud: "กลุ่มเมฆเซิร์ฟเวอร์" + federation: "Fediration" + instanceCloud: "อินสแตนซ์คลาวด์" postForm: "แบบฟอร์มการโพสต์" slideshow: "แสดงภาพนิ่ง" button: "ปุ่ม" onlineUsers: "ผู้ใช้ที่ออนไลน์" jobQueue: "คิวงาน" serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์" - aiscript: " คอนโซล AiScript" - aiscriptApp: "แอป AiScript" + aiscript: "AiScript คอนโซล" + aiscriptApp: "AiScript แอพ" aichan: "ไอ" userList: "รายชื่อผู้ใช้" _userList: - chooseList: "เลือกรายชื่อ" + chooseList: "เลือกรายการ" clicker: "คลิกเกอร์" - birthdayFollowings: "วันเกิดผู้ใช้ในวันนี้" _cw: hide: "ซ่อน" show: "โหลดเพิ่มเติม" @@ -2255,53 +1965,53 @@ _cw: files: "{count} ไฟล์" _poll: noOnlyOneChoice: "จำเป็นต้องมีอย่างน้อยสองตัวเลือก" - choiceN: "ตัวเลือกที่ {n}" - noMore: "เพิ่มตัวเลือกอีกไม่ได้แล้ว" + choiceN: "ตัวเลือก {n}" + noMore: "คุณไม่สามารถเพิ่มตัวเลือกอื่นได้" canMultipleVote: "สามารถตอบได้หลายคำตอบ" - expiration: "สิ้นสุดโพล" - infinite: "ไม่กำหนดระยะเวลา" - at: "ระบุวันเวลา" - after: "ระบุระยะเวลา" + expiration: "สิ้นสุดการสำรวจความคิดเห็น" + infinite: "ไม่ต้องเลย" + at: "จบที่..." + after: "สิ้นสุดหลัง..." deadlineDate: "วันสิ้นสุด" - deadlineTime: "เวลา" + deadlineTime: "ชั่วโมง" duration: "ระยะเวลา" votesCount: "{n} คะแนนเสียง" - totalVotes: "ทั้งหมด {n} คะแนนเสียง" + totalVotes: "{n} คะแนนเสียงทั้งหมด" vote: "โหวต" showResult: "ดูผลลัพธ์" voted: "โหวตแล้ว" closed: "สิ้นสุดแล้ว" - remainingDays: "เหลืออีก {d} วัน {h} ชั่วโมง" - remainingHours: "เหลืออีก {h} ชั่วโมง {m} นาที" - remainingMinutes: "เหลืออีก {m} นาที {s} วินาที" - remainingSeconds: "เหลืออีก {s} วินาที" + remainingDays: "{d} วัน(s) {h} ชั่วโมง(s) ที่เหลืออยู่" + remainingHours: "{h} ชั่วโมง(s) {m} นาที(s) ที่เหลืออยู่" + remainingMinutes: "{m} นาที(s) {s} วินาที(s) ที่เหลืออยู่" + remainingSeconds: "{s} นาที(s) ที่เหลืออยู่" _visibility: public: "สาธารณะ" publicDescription: "โน้ตของคุณจะปรากฏแก่ผู้ใช้ทุกคน" - home: "หน้าหลัก" - homeDescription: "โพสต์ลงไทม์ไลน์หลักเท่านั้น" + home: "หน้าแรก" + homeDescription: "โพสลงไทม์ไลน์ที่บ้านเท่านั้น" followers: "ผู้ติดตาม" - followersDescription: "เฉพาะผู้ติดตามเท่านั้นที่มองเห็นได้" + followersDescription: "ทำให้ผู้ติดตามนั้นมองเห็นแค่คุณเท่านั้น" specified: "ไดเร็ค" specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น" - disableFederation: "ไม่มีสหพันธ์" - disableFederationDescription: "อย่าส่งข้อมูลไปยังเซิร์ฟเวอร์อื่น" + disableFederation: "ไม่มีสหภาพ" + disableFederationDescription: "อย่าส่งไปยังอินสแตนซ์อื่น" _postForm: replyPlaceholder: "ตอบกลับโน้ตนี้..." quotePlaceholder: "อ้างโน้ตนี้..." channelPlaceholder: "โพสต์ลงช่อง..." _placeholders: - a: "ตอนนี้เป็นยังไงบ้าง?" - b: "มีอะไรเกิดขึ้นหรือเปล่า?" - c: "กำลังคิดอะไรอยู่?" - d: "ต้องการจะพูดอะไรไหม?" - e: "มาเขียนกันเถอะ" + a: "คุณเป็นอะไรไปหรอ?" + b: "เกิดอะไรขึ้นรอบตัวคุณ?" + c: "คุณกำลังคิดอะไรอยู่?" + d: "คุณต้องการจะพูดอะไร?" + e: "เริ่มเขียน..." f: "กำลังรอให้คุณเขียน..." _profile: name: "ชื่อ" username: "ชื่อผู้ใช้" - description: "แนะนำตัว" - youCanIncludeHashtags: "คุณสามารถใส่แฮชแท็กในส่วนแนะนำตัวของคุณได้" + description: "ประวัติ" + youCanIncludeHashtags: "คุณยังสามารถใส่แฮชแท็กในประวัติของคุณได้นะ" metadata: "ข้อมูลเพิ่มเติม" metadataEdit: "แก้ไขข้อมูลเพิ่มเติม" metadataDescription: "ใช้สิ่งเหล่านี้ คุณสามารถแสดงฟิลด์ข้อมูลเพิ่มเติมในโปรไฟล์ของคุณ" @@ -2309,66 +2019,64 @@ _profile: metadataContent: "เนื้อหา" changeAvatar: "เปลี่ยนอวาตาร์" changeBanner: "เปลี่ยนแบนเนอร์" - verifiedLinkDescription: "หากป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณ ไอคอนการยืนยันความเป็นเจ้าของจะแสดงถัดจากฟิลด์นั้น ๆ" - avatarDecorationMax: "คุณสามารถเพิ่มการตกแต่งได้สูงสุด {max}" + verifiedLinkDescription: "โดยการป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณตรงนี้ ส่วนไอคอนการยืนยันความเป็นเจ้าของนั้นก็สามารถแสดงถัดจากฟิลด์ได้นะ" _exportOrImport: allNotes: "โน้ตทั้งหมด" - favoritedNotes: "โน้ตที่ถูกใจไว้" - clips: "คลิป" + favoritedNotes: "บันทึกที่ชื่นชอบ" followingList: "กำลังติดตาม" muteList: "ปิดเสียง" blockingList: "บล็อค" - userLists: "รายชื่อ" + userLists: "รายการ" excludeMutingUsers: "ยกเว้นผู้ใช้ที่ปิดเสียง" excludeInactiveUsers: "ยกเว้นผู้ใช้ที่ไม่ได้ใช้งาน" - withReplies: "รวมการตอบกลับจากผู้ใช้ที่ถูกนำเข้า ลงไทม์ไลน์" + withReplies: "รวมการตอบกลับจากผู้ใช้ที่นำเข้าไว้ในไทม์ไลน์" _charts: federation: "สหพันธ์" apRequest: "คำขอ" - usersIncDec: "การเพิ่มลดของจำนวนผู้ใช้" + usersIncDec: "ความแตกต่างของจำนวนผู้ใช้งาน" usersTotal: "จำนวนผู้ใช้งานทั้งหมด" activeUsers: "จำนวนผู้ใช้งานที่ยังมีความเคลื่อนไหวอยู่" - notesIncDec: "การเพิ่มลดของจำนวนโน้ต" - localNotesIncDec: "การเพิ่มลดของจำนวนโน้ตท้องถิ่น" - remoteNotesIncDec: "การเพิ่มลดของจำนวนโน้ตระยะไกล" + notesIncDec: "ความแตกต่างของจำนวนโน้ต" + localNotesIncDec: "ความแตกต่างของจำนวนโน้ตท้องถิ่น" + remoteNotesIncDec: "ความแตกต่างของจำนวนโน้ตระยะไกล" notesTotal: "จำนวนโน้ตทั้งหมด" - filesIncDec: "การเพิ่มลดของจำนวนไฟล์" + filesIncDec: "ความแตกต่างของจำนวนไฟล์" filesTotal: "จำนวนไฟล์ทั้งหมด" - storageUsageIncDec: "การเพิ่มลดในการใช้พื้นที่เก็บข้อมูล" + storageUsageIncDec: "ความแตกต่างในการใช้พื้นที่เก็บข้อมูล" storageUsageTotal: "การใช้พื้นที่เก็บข้อมูลทั้งหมด" _instanceCharts: requests: "คำขอ" - users: "การเพิ่มลดของจำนวนผู้ใช้งาน" + users: "ความแตกต่างของจำนวนผู้ใช้งาน" usersTotal: "จำนวนผู้ใช้งานสะสม" - notes: "การเพิ่มลดของจำนวนโน้ต" + notes: "ความแตกต่างของจำนวนโน้ต" notesTotal: "จำนวนโน้ตสะสม" - ff: "การเพิ่มลดของการติดตาม/ผู้ติดตาม" - ffTotal: "จำนวนสะสมของการติดตาม/ผู้ติดตาม" - cacheSize: "การเพิ่มลดขนาดของแคช" - cacheSizeTotal: "ขนาดแคชสะสม" - files: "การเพิ่มลดของจำนวนไฟล์" + ff: "ความแตกต่างของจำนวนผู้ใช้ที่ติดตาม / ผู้ติดตาม" + ffTotal: "จำนวนผู้ใช้งานที่ติดตามสะสม / ผู้ติดตาม" + cacheSize: "ความแตกต่างในขนาดของแคช" + cacheSizeTotal: "ขนาดแคชรวมที่สะสม" + files: "ความแตกต่างของจำนวนไฟล์" filesTotal: "จำนวนไฟล์สะสม" _timelines: - home: "หน้าหลัก" - local: "ท้องถิ่น" - social: "โซเชียล" + home: "หน้าแรก" + local: "ในพื้นที่" + media: "สื่อ" + social: "โซเชี่ยล" global: "ทั่วโลก" _play: - new: "สร้าง Play" - edit: "แก้ไข Play" - created: "สร้าง Play แล้ว" - updated: "แก้ไข Play แล้ว" - deleted: "ลบ Play แล้ว" - pageSetting: "ตั้งค่า Play" + new: "สร้างการเล่น" + edit: "แก้ไขเล่น" + created: "สร้างการเล่นแล้ว" + updated: "แก้ไขการเล่นแล้ว" + deleted: "ลบการเล่นแล้ว" + pageSetting: "ตั้งค่าการเล่น" editThisPage: "แก้ไข Play นี้" viewSource: "ดูต้นฉบับ" - my: "Play ของฉัน" - liked: "Play ที่ถูกใจไว้" + my: "มาย เพลย์" + liked: "ไลค์ เพลย์" featured: "เป็นที่นิยม" title: "หัวข้อ" script: "สคริปต์" - summary: "คำอธิบาย" - visibilityDescription: "หากตั้งค่าเป็นส่วนตัว มันจะไม่ปรากฏในโปรไฟล์อีกต่อไป แต่ผู้ที่ทราบ URL ของมันจะยังสามารถเข้าถึงได้" + summary: "รายละเอียด" _pages: newPage: "สร้างหน้าเพจใหม่" editPage: "แก้ไขหน้าเพจ" @@ -2376,15 +2084,15 @@ _pages: created: "สร้างหน้าเพจสำเร็จเรียบร้อยแล้ว" updated: "แก้ไขหน้าเพจสำเร็จเรียบร้อยแล้ว" deleted: "ลบหน้าเพจสำเร็จเรียบร้อยแล้ว" - pageSetting: "การตั้งค่าหน้าเพจ" + pageSetting: "การตั้งค่าหน้า" nameAlreadyExists: "URL ของหน้าที่ระบุนั้นมีอยู่แล้ว" invalidNameTitle: "URL ของหน้าที่ระบุนั้นไม่ถูกต้อง" invalidNameText: "ตรวจสอบให้แน่ใจนะว่าชื่อหน้าไม่ว่างเปล่า" editThisPage: "แก้ไขเพจนี้" viewSource: "ดูต้นฉบับ" - viewPage: "ดูหน้าเพจ" + viewPage: "ดูหน้า" like: "ถูกใจ" - unlike: "เลิกถูกใจ" + unlike: "ลบไลค์" my: "หน้าเพจของฉัน" liked: "หน้าเพจที่ถูกใจ" featured: "เป็นที่นิยม" @@ -2397,7 +2105,7 @@ _pages: summary: "สรุปเพจ" alignCenter: "เซ็นเตอร์" hideTitleWhenPinned: "ซ่อนชื่อหน้าเพจเมื่อปักหมุดไว้ที่โปรไฟล์" - font: "แบบอักษร" + font: "ตัวอักษร" fontSerif: "Serif" fontSansSerif: "Sans Serif" eyeCatchingImageSet: "ตั้งค่าภาพขนาดย่อ" @@ -2405,7 +2113,7 @@ _pages: chooseBlock: "เพิ่มบล็อค" selectType: "เลือกชนิด" contentBlocks: "เนื้อหา" - inputBlocks: "ป้อนข้อมูล" + inputBlocks: "อินพุต" specialBlocks: "พิเศษ" blocks: text: "ข้อความ" @@ -2413,8 +2121,6 @@ _pages: section: "ประเภท" image: "รูปภาพ" button: "ปุ่ม" - dynamic: "บล็อกแบบไดนามิก" - dynamicDescription: "บล็อกนี้ล้าสมัยแล้ว โปรดใช้ {play} แทน นับจากนี้เป็นต้นไป" note: "โน้ตที่ฝังตัว" _note: id: "โน้ต ID" @@ -2425,30 +2131,24 @@ _relayStatus: accepted: "ได้รับการอนุมัติ" rejected: "ถูกปฏิเสธ" _notification: - fileUploaded: "ไฟล์ถูกอัปโหลดแล้ว" + fileUploaded: "ไฟล์ถูกอัพโหลดแล้วน่ะ" youGotMention: "{name} กล่าวถึงคุณ" youGotReply: "{name} ตอบกลับถึงคุณ" - youGotQuote: "{name} อ้างอิงคุณ" + youGotQuote: "{name} อ้างถึงคุณ" youRenoted: "รีโน้ตจาก {name}" youWereFollowed: "ได้ติดตามคุณ" - youReceivedFollowRequest: "ได้รับคำขอติดตาม" - yourFollowRequestAccepted: "คำขอติดตามได้รับการอนุมัติแล้ว" + youReceivedFollowRequest: "คุณมีคำขอติดตามใหม่น่ะ" + yourFollowRequestAccepted: "คำขอติดตามของคุณได้รับการยอมรับแล้วน่ะ" youWereInvitedToGroup: "{userName} ได้เชิญคุณเข้ากลุ่ม" - pollEnded: "ผลโพลออกมาแล้ว" + pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน" newNote: "โพสต์ใหม่" unreadAntennaNote: "เสาอากาศ {name}" - roleAssigned: "ได้รับบทบาท" - emptyPushNotificationMessage: "อัปเดตการแจ้งเตือนแบบพุชแล้ว" + emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว" achievementEarned: "รับความสำเร็จ" testNotification: "ทดสอบการแจ้งเตือน" - checkNotificationBehavior: "กดเพื่อดูลักษณะการแจ้งเตือน" + checkNotificationBehavior: "ตรวจสอบลักษณะที่ปรากฏการแจ้งเตือน" sendTestNotification: "ส่งทดสอบการแจ้งเตือน" notificationWillBeDisplayedLikeThis: "การแจ้งเตือนมีลักษณะแบบนี้" - reactedBySomeUsers: "ถูกรีแอคชั่นโดยผู้ใช้ {n} ราย" - likedBySomeUsers: "{n} คนถูกใจ" - renotedBySomeUsers: "รีโน้ตจากผู้ใช้ {n} ราย" - followedBySomeUsers: "มีผู้ติดตาม {n} ราย" - flushNotification: "ล้างประวัติการแจ้งเตือน" _types: all: "ทั้งหมด" note: "โน้ตใหม่" @@ -2456,13 +2156,12 @@ _notification: mention: "กล่าวถึง" reply: "ตอบกลับ" renote: "รีโน้ต" - quote: "อ้างอิง" + quote: "อ้างคำพูด" reaction: "รีแอคชั่น" - pollEnded: "โพลสิ้นสุดแล้ว" - receiveFollowRequest: "ได้รับคำร้องขอติดตาม" - followRequestAccepted: "อนุมัติให้ติดตามแล้ว" + pollEnded: "โพลนี้สิ้นสุดลงแล้ว" + receiveFollowRequest: "ได้รับคำขอติดตาม\n" + followRequestAccepted: "ยอมรับคำขอติดตาม" groupInvited: "ได้รับคำเชิญเข้ากลุ่ม" - roleAssigned: "ให้บทบาท" achievementEarned: "ปลดล็อกความสำเร็จแล้ว" app: "การแจ้งเตือนจากแอปที่มีลิงก์" _actions: @@ -2473,7 +2172,6 @@ _deck: alwaysShowMainColumn: "แสดงคอลัมน์หลักเสมอ" columnAlign: "จัดแนวคอลัมน์" addColumn: "เพิ่มคอลัมน์" - newNoteNotificationSettings: "ตั้งค่าการแจ้งเตือนเมื่อมีโน้ตใหม่" configureColumn: "ตั้งค่าคอลัมน์" swapLeft: "ขยับไปทางซ้าย" swapRight: "ขยับไปทางขวา" @@ -2497,9 +2195,9 @@ _deck: tl: "ไทม์ไลน์" antenna: "เสาอากาศ" list: "รายการ" - channel: "ช่อง" - mentions: "กล่าวถึงคุณ" - direct: "ไดเร็กต์" + channel: "แชนแนล" + mentions: "พูดถึง" + direct: "ไดเร็ค" roleTimeline: "บทบาทไทม์ไลน์" _dialog: charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" @@ -2512,9 +2210,9 @@ _drivecleaner: orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก" _webhookSettings: createWebhook: "สร้าง Webhook" - modifyWebhook: "แก้ไข Webhook" name: "ชื่อ" secret: "ความลับ" + events: "อีเว้นท์ Webhook" active: "เปิดใช้งาน" _events: follow: "เมื่อกำลังติดตามผู้ใช้" @@ -2524,37 +2222,17 @@ _webhookSettings: renote: "รีโน้ตแล้วเมื่อ" reaction: "เมื่อได้รับรีแอคชั่น" mention: "เมื่อกำลังถูกกล่าวถึง" - _systemEvents: - abuseReport: "เมื่อมีการรายงานจากผู้ใช้" - abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้" - userCreated: "เมื่อผู้ใช้ถูกสร้างขึ้น" - deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?" -_abuseReport: - _notificationRecipient: - createRecipient: "เพิ่มปลายทางการแจ้งเตือนการรายงาน" - modifyRecipient: "แก้ไขปลายทางการแจ้งเตือนการรายงาน" - recipientType: "ประเภทของปลายทางการแจ้งเตือน\n" - _recipientType: - mail: "อีเมล" - webhook: "Webhook" - _captions: - mail: "ส่งการแจ้งเตือนไปยังที่อยู่อีเมลของผู้ควบคุม (เฉพาะเมื่อได้รับการรายงาน)" - webhook: "ส่งการแจ้งเตือนไปยัง SystemWebhook ที่กำหนด (จะส่งเมื่อได้รับการรายงานและเมื่อการรายงานได้รับการแก้ไข)" - keywords: "คีย์เวิร์ด" - notifiedUser: "ผู้ใช้ที่ได้รับการแจ้งเตือน" - notifiedWebhook: "Webhook ที่ใช้" - deleteConfirm: "ต้องการลบปลายทางการแจ้งเตือนใช่ไหม?" _moderationLogTypes: createRole: "สร้างบทบาทแล้ว" deleteRole: "ลบบทบาทแล้ว" updateRole: "อัปเดตบทบาทแล้ว" assignRole: "ได้รับมอบหมายบทบาท" unassignRole: "ถอดออกจากบทบาทแล้ว" - suspend: "ระงับ" - unsuspend: "เลิกระงับ" - addCustomEmoji: "เพิ่มเอโมจิที่กำหนดเองแล้ว" - updateCustomEmoji: "อัปเดตเอโมจิที่กำหนดเองแล้ว" - deleteCustomEmoji: "ลบเอโมจิที่กำหนดเองออกแล้ว" + suspend: "ถูกระงับ" + unsuspend: "เลิกถูกระงับ" + addCustomEmoji: "เพิ่มอีโมจิที่กำหนดเองแล้ว" + updateCustomEmoji: "อัปเดตอีโมจิที่กำหนดเองแล้ว" + deleteCustomEmoji: "ลบอีโมจิที่กำหนดเองออกแล้ว" updateServerSettings: "อัปเดตการตั้งค่าเซิร์ฟเวอร์แล้ว" updateUserNote: "อัปเดตโน้ตการกลั่นกรองแล้ว" deleteDriveFile: "ลบไฟล์ออกแล้ว" @@ -2566,27 +2244,15 @@ _moderationLogTypes: deleteGlobalAnnouncement: "ลบประกาศทั่วโลกออกแล้ว" deleteUserAnnouncement: "ลบประกาศผู้ใช้ออกแล้ว" resetPassword: "รีเซ็ตรหัสผ่าน" - suspendRemoteInstance: "ระงับเซิร์ฟเวอร์ระยะไกล" - unsuspendRemoteInstance: "เลิกระงับเซิร์ฟเวอร์ระยะไกล" - updateRemoteInstanceNote: "อัปเดตโน้ตการกลั่นกรองสำหรับเซิร์ฟเวอร์ระยะไกลแล้ว" - markSensitiveDriveFile: "ทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน" - unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่ามีเนื้อหาละเอียดอ่อน" + suspendRemoteInstance: "อินสแตนซ์ระยะไกลถูกระงับ" + unsuspendRemoteInstance: "อินสแตนซ์ระยะไกลเลิกการระงับ" + markSensitiveDriveFile: "ทำเครื่องหมายไฟล์บอกว่าละเอียดอ่อน" + unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่าละเอียดอ่อน" resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว" - createInvitation: "สร้างรหัสเชิญ" + createInvitation: "สร้างคำเชิญ" createAd: "สร้างโฆษณาแล้ว" deleteAd: "ลบโฆษณาออกแล้ว" updateAd: "อัปเดตโฆษณาแล้ว" - createAvatarDecoration: "สร้างการตกแต่งไอคอนแล้ว" - updateAvatarDecoration: "อัปเดตการตกแต่งไอคอนแล้ว" - deleteAvatarDecoration: "ลบการตกแต่งไอคอนแล้ว" - unsetUserAvatar: "ลบไอคอนผู้ใช้" - unsetUserBanner: "ลบแบนเนอร์ผู้ใช้" - createSystemWebhook: "สร้าง SystemWebhook" - updateSystemWebhook: "อัปเดต SystemWebhook" - deleteSystemWebhook: "ลบ SystemWebhook" - createAbuseReportNotificationRecipient: "สร้างปลายทางการแจ้งเตือนการรายงาน" - updateAbuseReportNotificationRecipient: "อัปเดตปลายทางการแจ้งเตือนการรายงาน" - deleteAbuseReportNotificationRecipient: "ลบปลายทางการแจ้งเตือนการรายงาน" _fileViewer: title: "รายละเอียดไฟล์" type: "ประเภทไฟล์" @@ -2596,130 +2262,14 @@ _fileViewer: attachedNotes: "โน้ตที่แนบมาด้วย" thisPageCanBeSeenFromTheAuthor: "หน้าเพจนี้จะสามารถปรากฏได้โดยผู้ใช้ที่อัปโหลดไฟล์นี้เท่านั้น" _externalResourceInstaller: - title: "ติดตั้งจากไซต์ภายนอก" - checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง" _plugin: - title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?" metaTitle: "ข้อมูลส่วนเสริม" _theme: - title: "ต้องการติดตั้งธีมนี้ใช่ไหม?" metaTitle: "ข้อมูลธีม" - _meta: - base: "โทนสีพื้นฐาน" _vendorInfo: title: "ข้อมูลผู้จัดจำหน่าย" - endpoint: "จุดอ้างอิงปลายทาง (Referenced endpoint)" - hashVerify: "การตรวจสอบแฮช (ความสมบูรณ์ของไฟล์)" _errors: - _invalidParams: - title: "พารามิเตอร์ไม่ถูกต้อง" - description: "มีสารสนเทศไม่เพียงพอที่จะโหลดข้อมูลจากไซต์ภายนอก โปรดยืนยัน URL ที่ป้อน" - _resourceTypeNotSupported: - title: "ไม่รองรับทรัพยากรภายนอกนี้" - description: "ไม่รองรับประเภทของทรัพยากรภายนอกนี้ โปรดติดต่อผู้ดูแลเว็บไซต์" - _failedToFetch: - title: "รับข้อมูลล้มเหลว" - fetchErrorDescription: "เกิดข้อผิดพลาดในการสื่อสารกับไซต์ภายนอก หากการลองอีกครั้งไม่สามารถแก้ไขปัญหานี้ได้ โปรดติดต่อผู้ดูแลไซต์" - parseErrorDescription: "เกิดข้อผิดพลาดในการประมวลผลข้อมูลที่โหลดจากไซต์ภายนอก โปรดติดต่อผู้ดูแลเว็บไซต์" - _hashUnmatched: - title: "การยืนยัน/ตรวจสอบข้อมูลล้มเหลว" - description: "เกิดข้อผิดพลาดในการตรวจสอบความสมบูรณ์ของข้อมูลที่ดึงมา เพื่อเป็นมาตรการรักษาความปลอดภัย การติดตั้งไม่สามารถดำเนินการต่อได้ โปรดติดต่อผู้ดูแลเว็บไซต์" _pluginParseFailed: title: "ข้อผิดพลาด AiScript" - description: "ดึงข้อมูลที่ร้องขอสำเร็จแล้ว แต่มีข้อผิดพลาดเกิดขึ้นระหว่างการแยกวิเคราะห์ AiScript โปรดติดต่อผู้เขียนปลั๊กอิน รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript" - _pluginInstallFailed: - title: "ติดตั้งปลั๊กอินล้มเหลว" - description: "เกิดปัญหาขณะติดตั้งปลั๊กอิน กรุณาลองอีกครั้ง. โปรดดูคอนโซล Javascript สำหรับรายละเอียดข้อผิดพลาด" _themeParseFailed: title: "การแยกวิเคราะห์ธีมล้มเหลว" - description: "ดึงข้อมูลที่ร้องขอสำเร็จแล้ว แต่มีข้อผิดพลาดเกิดขึ้นระหว่างการแยกวิเคราะห์ธีม โปรดติดต่อผู้เขียนธีม รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript" - _themeInstallFailed: - title: "ติดตั้งธีมล้มเหลว" - description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript" -_dataSaver: - _media: - title: "โหลดสื่อ" - description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด" - _avatar: - title: "รูปไอคอน" - description: "ระงับการเคลื่อนไหวของภาพไอคอน ภาพเคลื่อนไหวอาจมีขนาดไฟล์ใหญ่กว่าภาพปกติ ดังนั้นจึงสามารถช่วยในการลดการใช้ข้อมูล" - _urlPreview: - title: "ธัมบ์เนลแสดงตัวอย่าง URL" - description: "ธัมบ์เนลแสดงตัวอย่าง URL จะไม่โหลดโดยอัตโนมัติ" - _code: - title: "ไฮไลต์โค้ด" - description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้" -_hemisphere: - N: "ซีกโลกเหนือ" - S: "ซีกโลกใต้" - caption: "ใช้เพื่อกำหนดฤดูกาลของไคลเอ็นต์" -_reversi: - reversi: "รีเวอร์ซี" - gameSettings: "ตั้งค่าการเล่น" - chooseBoard: "เลือกกระดาน" - blackOrWhite: "ดำ/ขาว" - blackIs: "{name}เป็นสีดำ" - rules: "กฎ" - thisGameIsStartedSoon: "การเล่นจะเริ่มแล้ว" - waitingForOther: "กำลังรออีกฝ่ายเตรียมตัวให้เสร็จ" - waitingForMe: "กำลังรอฝ่ายคุณเตรียมตัวให้เสร็จ" - waitingBoth: "กรุณาเตรียมตัว" - ready: "เตรียมตัวพร้อมแล้ว" - cancelReady: "ยกเลิกการเตรียมตัวพร้อม" - opponentTurn: "ตาอีกฝ่าย" - myTurn: "ตาของคุณ" - turnOf: "ตาของ{name}" - pastTurnOf: "ตาของ{name}" - surrender: "ยอมแพ้" - surrendered: "ยอมแพ้แล้ว" - timeout: "หมดเวลาแล้ว" - drawn: "เสมอ" - won: "{name}ชนะ" - black: "ดำ" - white: "ขาว" - total: "รวมทั้งหมด" - turnCount: "ตาที่{count}" - myGames: "การเล่นของตัวเอง" - allGames: "การเล่นของทุกคน" - ended: "จบ" - playing: "กำลังเล่น" - isLlotheo: "คนที่มีตัวหมากน้อยกว่าชนะ (Roseo)" - loopedMap: "ลูปแมป" - canPutEverywhere: "โหมดที่สามารถวางได้ทุกที่" - timeLimitForEachTurn: "จำกัดเวลาต่อแต่ละตา" - freeMatch: "ฟรีแมตช์" - lookingForPlayer: "กำลังมองหาคู่ต่อสู้อยู่" - gameCanceled: "ยกเลิกการเล่นแล้ว" - shareToTlTheGameWhenStart: "โพสต์ลงไทม์ไลน์เมื่อเริ่มการเล่น" - iStartedAGame: "เริ่มเล่นหมากรีเวอร์ซีแล้ว! #MisskeyReversi" - opponentHasSettingsChanged: "อีกฝ่ายเปลี่ยนการตั้งค่า" - allowIrregularRules: "อนุญาตกฎที่ไม่ปรกติ (โหมดฟรีทุกอย่าง)" - disallowIrregularRules: "ไม่อนุญาตกฎที่ไม่ปรกติ" - showBoardLabels: "แสดงหมายเลขแถว/คอลัมน์บนกระดาน" - useAvatarAsStone: "ใช้รูปอวตารเป็นหมาก" -_offlineScreen: - title: "ออฟไลน์ - ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" - header: "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" -_urlPreviewSetting: - title: "การตั้งค่าการแสดงตัวอย่าง URL" - enable: "เปิดใช้งานการแสดงตัวอย่าง URL" - timeout: "เวลาจำกัดในการโหลดตัวอย่าง URL (ms)" - timeoutDescription: "หากเวลาที่ใช้ในการโหลดเกินค่านี้ จะไม่มีการสร้างการแสดงตัวอย่าง" - maximumContentLength: "ค่าสูงสุดของ Content-Length (byte)" - maximumContentLengthDescription: "หาก Content-Length เกินค่านี้ จะไม่มีการสร้างการแสดงตัวอย่าง" - requireContentLength: "สร้างการแสดงตัวอย่างเฉพาะในกรณีที่รับ Content-Length ไหว" - requireContentLengthDescription: "หากเซิร์ฟเวอร์อื่นไม่ส่งคืน Content-Length จะไม่มีการสร้างการแสดงตัวอย่าง" - userAgent: "User-Agent" - userAgentDescription: "ตั้งค่า User-Agent ที่ใช้ในการรับการแสดงตัวอย่าง หากเว้นว่างไว้ ระบบจะใช้ User-Agent เริ่มต้น" - summaryProxy: "endpoint ของพร็อกซีที่สร้างการแสดงตัวอย่าง" - summaryProxyDescription: "สร้างการแสดงตัวอย่างด้วย summary Proxy แทนที่จะใช้เนื้อหา CherryPick" - summaryProxyDescription2: "พารามิเตอร์ต่อไปนี้จะถูกใช้เป็นสตริงการสืบค้นเพื่อเชื่อมต่อกับพร็อกซี หากฝั่งพร็อกซีไม่รองรับการตั้งค่าเหล่านี้จะถูกละเว้น" -_mediaControls: - pip: "รูปภาพในรูปภาม" - playbackRate: "ความเร็วในการเล่น" - loop: "เล่นวนซ้ำ" -_contextMenu: - title: "เมนูเนื้อหา" - app: "แอปพลิเคชัน" - appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)" - native: "UI ของเบราว์เซอร์" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index d659833313..120b9f4230 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -378,10 +378,6 @@ addMemo: "Kısa not ekle" icon: "Avatar" replies: "yanıt" renotes: "vazgeçme" -_delivery: - stop: "Askıya alınmış" - _type: - none: "Paylaşım" _accountDelete: started: "Silme işlemi başlatıldı" _email: diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml index e06cee11a2..e48f64511c 100644 --- a/locales/ug-CN.yml +++ b/locales/ug-CN.yml @@ -17,4 +17,3 @@ _2fa: renewTOTPCancel: "ئۇنى توختىتىڭ" _widgets: profile: "profile" - diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index e35e2b067d..73ce06a155 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -352,8 +352,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Увімкнути hCaptcha" hcaptchaSiteKey: "Ключ сайту" hcaptchaSecretKey: "Секретний ключ" -mcaptchaSiteKey: "Ключ сайту" -mcaptchaSecretKey: "Секретний ключ" recaptcha: "reCAPTCHA" enableRecaptcha: "Увімкнути reCAPTCHA" recaptchaSiteKey: "Ключ сайту" @@ -925,13 +923,7 @@ youFollowing: "Підписки" icon: "Аватар" replies: "Відповісти" renotes: "Поширити" -sourceCode: "Вихідний код" flip: "Перевернути" -lastNDays: "Останні {n} днів" -_delivery: - stop: "Призупинено" - _type: - none: "Публікація" _achievements: earnedAt: "Відкрито" _types: @@ -988,7 +980,7 @@ _achievements: _login3: title: "Новачок I" description: "3 дні користування загально" - flavor: "Відсьогодні називайте мене \"Черрипикіст\"" + flavor: "Відсьогодні називайте мене \"Місскіст\"" _login7: title: "Новачок II" description: "7 днів користування загально" @@ -1396,6 +1388,8 @@ _sfx: notification: "Сповіщення" chat: "Чати" chatBg: "Чати (фон)" + antenna: "Прийом антени" + channel: "Повідомлення каналу" _ago: future: "Майбутнє" justNow: "Щойно" @@ -1552,7 +1546,6 @@ _profile: changeBanner: "Змінити банер" _exportOrImport: allNotes: "Всі нотатки" - clips: "Добірка" followingList: "Підписки" muteList: "Ігнорувати" blockingList: "Заблокувати" @@ -1701,12 +1694,6 @@ _deck: _webhookSettings: name: "Ім'я" active: "Увімкнено" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "E-mail" _moderationLogTypes: suspend: "Призупинити" resetPassword: "Скинути пароль" -_reversi: - total: "Всього" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 36cea2278f..162eef6fa8 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -366,8 +366,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptchani yoqish" hcaptchaSiteKey: "Sayt kaliti" hcaptchaSecretKey: "Mahfiy kalit" -mcaptchaSiteKey: "Sayt kaliti" -mcaptchaSecretKey: "Maxfiy kalit" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHA ni yoqish" recaptchaSiteKey: "Sayt kaliti" @@ -846,10 +844,6 @@ icon: "Avatar" replies: "Javob berish" renotes: "Qayta qayd etish" flip: "Teskari" -_delivery: - stop: "To'xtatilgan" - _type: - none: "Yuborilmoqda" _achievements: _types: _viewInstanceChart: @@ -980,7 +974,6 @@ _profile: changeBanner: "Bannerni o'zgartirish" _exportOrImport: allNotes: "Barcha qaydlar" - clips: "Klip" followingList: "Obuna bo‘lish" muteList: "Ovozni o‘chirish" blockingList: "Bloklangan foydalanuvchilar" @@ -1090,12 +1083,6 @@ _webhookSettings: _events: renote: "Qayta qayd qilinganda" mention: "Eslanganda" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" _moderationLogTypes: suspend: "To'xtatish" resetPassword: "Parolni tiklash" -_reversi: - total: "Jami" diff --git a/locales/verify.js b/locales/verify.js deleted file mode 100644 index a8e9875d6e..0000000000 --- a/locales/verify.js +++ /dev/null @@ -1,53 +0,0 @@ -import locales from './index.js'; - -let valid = true; - -function writeError(type, lang, tree, data) { - process.stderr.write(JSON.stringify({ type, lang, tree, data })); - process.stderr.write('\n'); - valid = false; -} - -function verify(expected, actual, lang, trace) { - for (let key in expected) { - if (!Object.prototype.hasOwnProperty.call(actual, key)) { - continue; - } - if (typeof expected[key] === 'object') { - if (typeof actual[key] !== 'object') { - writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'object', actual: typeof actual[key] }); - continue; - } - verify(expected[key], actual[key], lang, trace ? `${trace}.${key}` : key); - } else if (typeof expected[key] === 'string') { - switch (typeof actual[key]) { - case 'object': - writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'string', actual: 'object' }); - break; - case 'undefined': - continue; - case 'string': - const expectedParameters = new Set(expected[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1))); - const actualParameters = new Set(actual[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1))); - for (let parameter of expectedParameters) { - if (!actualParameters.has(parameter)) { - writeError('missing_parameter', lang, trace ? `${trace}.${key}` : key, { parameter }); - } - } - } - } - } -} - -const { ['ja-JP']: original, ...verifiees } = locales; - -for (let lang in verifiees) { - if (!Object.prototype.hasOwnProperty.call(locales, lang)) { - continue; - } - verify(original, verifiees[lang], lang); -} - -if (!valid) { - process.exit(1); -} diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 1a1df6e0e8..1c4313aef5 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -121,11 +121,9 @@ sensitive: "Nhạy cảm" add: "Thêm" reaction: "Biểu cảm" reactions: "Biểu cảm" -emojiPicker: "Bộ chọn biểu tượng cảm xúc" reactionSettingDescription2: "Kéo để sắp xếp, nhấn để xóa, nhấn \"+\" để thêm." rememberNoteVisibility: "Lưu kiểu tút mặc định" attachCancel: "Gỡ tập tin đính kèm" -deleteFile: "Xoá tệp tin" markAsSensitive: "Đánh dấu là nhạy cảm" unmarkAsSensitive: "Bỏ đánh dấu nhạy cảm" enterFileName: "Nhập tên tập tin" @@ -259,7 +257,6 @@ removed: "Đã xóa" removeAreYouSure: "Bạn có chắc muốn gỡ \"{x}\"?" deleteAreYouSure: "Bạn có chắc muốn xóa \"{x}\"?" resetAreYouSure: "Bạn có chắc muốn đặt lại?" -areYouSure: "Bạn chắc chứ?" saved: "Đã lưu" messaging: "Trò chuyện" upload: "Tải lên" @@ -310,7 +307,6 @@ folderName: "Tên thư mục" createFolder: "Tạo thư mục" renameFolder: "Đổi tên thư mục" deleteFolder: "Xóa thư mục" -folder: "Thư mục" addFile: "Thêm tập tin" emptyDrive: "Ổ đĩa của bạn trống trơn" emptyFolder: "Thư mục trống" @@ -372,11 +368,6 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Bật hCaptcha" hcaptchaSiteKey: "Khóa của trang" hcaptchaSecretKey: "Khóa bí mật" -mcaptcha: "mCaptcha" -enableMcaptcha: "Bật mCaptcha" -mcaptchaSiteKey: "Khóa của trang" -mcaptchaSecretKey: "Khóa bí mật" -mcaptchaInstanceUrl: "URL mCaptcha máy chủ" recaptcha: "reCAPTCHA" enableRecaptcha: "Bật reCAPTCHA" recaptchaSiteKey: "Khóa của trang" @@ -392,7 +383,6 @@ name: "Tên" antennaSource: "Nguồn trạm phát sóng" antennaKeywords: "Từ khóa để nghe" antennaExcludeKeywords: "Từ khóa để lọc ra" -antennaExcludeBots: "Loại trừ các tài khoản bot" antennaKeywordsDescription: "Phân cách bằng dấu cách cho điều kiện AND hoặc bằng xuống dòng cho điều kiện OR." notifyAntenna: "Thông báo có tút mới" withFileAntenna: "Chỉ những tút có media" @@ -427,7 +417,6 @@ moderator: "Kiểm duyệt viên" moderation: "Kiểm duyệt" moderationNote: "Ghi chú kiểm duyệt" addModerationNote: "Thêm ghi chú kiểm duyệt" -moderationLogs: "Nhật kí quản trị" nUsersMentioned: "Dùng bởi {n} người" securityKeyAndPasskey: "Mã bảo mật・Passkey" securityKey: "Khóa bảo mật" @@ -469,7 +458,6 @@ noteOf: "Tút của {user}" inviteToGroup: "Mời vào nhóm" quoteAttached: "Trích dẫn" quoteQuestion: "Trích dẫn lại?" -attachAsFileQuestion: "Văn bản ở trong bộ nhớ tạm rất dài. Bạn có muốn đăng nó dưới dạng một tệp văn bản không?" noMessagesYet: "Chưa có tin nhắn" newMessageExists: "Bạn có tin nhắn mới" onlyOneFileCanBeAttached: "Bạn chỉ có thể đính kèm một tập tin" @@ -559,7 +547,6 @@ showInPage: "Hiện trong trang" popout: "Pop-out" volume: "Âm lượng" masterVolume: "Âm thanh chung" -notUseSound: "Tắt tiếng" details: "Chi tiết" chooseEmoji: "Chọn emoji" unableToProcess: "Không thể hoàn tất hành động" @@ -580,10 +567,6 @@ output: "Nguồn ra" script: "Kịch bản" disablePagesScript: "Tắt AiScript trên Trang" updateRemoteUser: "Cập nhật thông tin người dùng ở máy chủ khác" -unsetUserAvatar: "Gỡ ảnh đại diện" -unsetUserAvatarConfirm: "Bạn có chắc muốn gỡ ảnh đại diện?" -unsetUserBanner: "Gỡ ảnh bìa" -unsetUserBannerConfirm: "Bạn có chắc muốn gỡ ảnh bìa?" deleteAllFiles: "Xóa toàn bộ tập tin" deleteAllFilesConfirm: "Bạn có chắc xóa toàn bộ tập tin?" removeAllFollowing: "Ngưng theo dõi tất cả mọi người" @@ -886,8 +869,6 @@ makeReactionsPublicDescription: "Điều này sẽ hiển thị công khai danh classic: "Cổ điển" muteThread: "Không quan tâm nữa" unmuteThread: "Quan tâm tút này" -followingVisibility: "Hiển thị lượt theo dõi" -followersVisibility: "Hiển thị người theo dõi" continueThread: "Tiếp tục xem chuỗi tút" deleteAccountConfirm: "Điều này sẽ khiến tài khoản bị xóa vĩnh viễn. Vẫn tiếp tục?" incorrectPassword: "Sai mật khẩu." @@ -999,7 +980,6 @@ assign: "Phân công" unassign: "Hủy phân công" color: "Màu sắc" manageCustomEmojis: "Quản lý CustomEmoji" -manageAvatarDecorations: "Quản lý trang trí ảnh đại diện" youCannotCreateAnymore: "Bạn đã tới giới hạn tạo." cannotPerformTemporary: "Tạm thời không sử dụng được" cannotPerformTemporaryDescription: "Tạm thời không sử dụng được vì lần số điều kiện quá giới hạn. Thử lại sau mọt lát nữa." @@ -1023,24 +1003,18 @@ copyErrorInfo: "Sao chép thông tin lỗi" joinThisServer: "Đăng ký trên chủ máy này" exploreOtherServers: "Tìm chủ máy khác" letsLookAtTimeline: "Thử xem Timeline" -disableFederationOk: "Vô hiệu hoá" emailNotSupported: "Máy chủ này không hỗ trợ gửi email" postToTheChannel: "Đăng lên kênh" cannotBeChangedLater: "Không thể thay đổi sau này." -likeOnly: "Chỉ lượt thích" rolesAssignedToMe: "Vai trò được giao cho tôi" resetPasswordConfirm: "Bạn thực sự muốn đặt lại mật khẩu?" sensitiveWords: "Các từ nhạy cảm" -prohibitedWords: "Các từ bị cấm" license: "Giấy phép" unfavoriteConfirm: "Bạn thực sự muốn xoá khỏi mục yêu thích?" -retryAllQueuesConfirmTitle: "Bạn có muốn thử lại?" retryAllQueuesConfirmText: "Điều này sẽ tạm thời làm tăng mức độ tải của máy chủ." enableChartsForRemoteUser: "Tạo biểu đồ người dùng từ xa" video: "Video" videos: "Các video" -audio: "Âm thanh" -audioFiles: "Âm thanh" dataSaver: "Tiết kiệm dung lượng" accountMigration: "Chuyển tài khoản" accountMoved: "Người dùng này đã chuyển sang một tài khoản mới:" @@ -1057,88 +1031,33 @@ vertical: "Dọc" horizontal: "Thanh bên" position: "Vị trí" serverRules: "Luật của máy chủ" -pleaseConfirmBelowBeforeSignup: "Để đăng ký trên máy chủ này, bạn phải xem xét và đồng ý với những điều sau." -pleaseAgreeAllToContinue: "Bạn phải đồng ý tất cả điều trên để tiếp tục." -continue: "Tiếp tục" -archive: "Lưu trữ" -thisChannelArchived: "Kênh này đã được lưu trữ." -initialAccountSetting: "Thiết lập hồ sơ" youFollowing: "Đang theo dõi" -preventAiLearning: "Từ chối sử dụng công nghệ Máy Học (AI Sáng Tạo)" -options: "Tùy chọn" -specifyUser: "Người dùng chỉ định" -failedToPreviewUrl: "Không thể xem trước" -update: "Cập nhật" later: "Để sau" goToMisskey: "Tới CherryPick" installed: "Đã tải xuống" branding: "Thương hiệu" turnOffToImprovePerformance: "Tắt mục này có thể cải thiện hiệu năng." -createInviteCode: "Tạo lời mời" -createWithOptions: "Tạo cùng tùy chọn" -createCount: "Số lượng mời" -inviteCodeCreated: "Lời mời đã được tạo" -inviteLimitExceeded: "Bạn đã vượt quá số lượng mời mà bạn có thể tạo." -createLimitRemaining: "Giới hạn lượt mời: Còn lại {limit}" -inviteLimitResetCycle: "Giới hạn này sẽ được đặt lại về {limit} lúc {time}." expirationDate: "Ngày hết hạn" noExpirationDate: "Vô thời hạn" -inviteCodeUsedAt: "Mã mời đã được sử dụng lúc" -registeredUserUsingInviteCode: "Lời mời đã được sử dụng bởi" waitingForMailAuth: "Đang chờ xác nhận email" -inviteCodeCreator: "Lời mời đã được tạo bởi" -usedAt: "Sử dụng vào lúc" unused: "Chưa được sử dụng" used: "Đã được sử dụng" expired: "Đã hết hạn" doYouAgree: "Đồng ý?" -beSureToReadThisAsItIsImportant: "Hãy đọc kỹ vì nó rất quan trọng." -iHaveReadXCarefullyAndAgree: "Tôi đã đọc và đồng ý với \"{x}\"." +iHaveReadXCarefullyAndAgree: "Tôi đã đọc và đồng ý với \"x\"." dialog: "Hộp thoại" icon: "Ảnh đại diện" forYou: "Dành cho bạn" currentAnnouncements: "Thông báo hiện tại" pastAnnouncements: "Thông báo trước đó" youHaveUnreadAnnouncements: "Có thông báo chưa đọc." -useSecurityKey: "Làm theo hướng dẫn trên trình duyệt hoặc thiết bị của bạn để sử dụng khóa bảo mật hoặc mật mã." replies: "Trả lời" renotes: "Đăng lại" loadReplies: "Hiển thị các trả lời" -loadConversation: "Xem cuộc trò chuyện" pinnedList: "Các mục đã được ghim" keepScreenOn: "Giữ màn hình luôn bật" verifiedLink: "Chúng tôi đã xác nhận bạn là chủ sở hữu của đường dẫn này" -authentication: "Xác thực" -authenticationRequiredToContinue: "Vui lòng xác thực để tiếp tục" -dateAndTime: "Ngày và giờ" -edited: "Đã chỉnh sửa" -notificationRecieveConfig: "Cài đặt thông báo" -mutualFollow: "Theo dõi lẫn nhau" -followingOrFollower: "Đang theo dõi hoặc người theo dõi" -externalServices: "Các dịch vụ bên ngoài" -sourceCode: "Mã nguồn" -feedback: "Phản hồi" -feedbackUrl: "URL phản hồi" -privacyPolicy: "Chính sách bảo mật" -privacyPolicyUrl: "URL Chính sách bảo mật" -tosAndPrivacyPolicy: "Điều khoản sử dụng và Chính sách bảo mật" -avatarDecorations: "Trang trí ảnh đại diện" -attach: "Mặc" -detach: "Bỏ" -detachAll: "Bỏ tất cả" -angle: "Góc" flip: "Lật" -showAvatarDecorations: "Hiển thị trang trí ảnh đại diện" -releaseToRefresh: "Thả để làm mới" -refreshing: "Đang làm mới" -pullDownToRefresh: "Kéo xuống để làm mới" -cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải có chú thích." -lastNDays: "{n} ngày trước" -surrender: "Từ chối" -_delivery: - stop: "Đã vô hiệu hóa" - _type: - none: "Đang đăng" _announcement: forExistingUsers: "Chỉ những người dùng đã tồn tại" forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó." @@ -1217,7 +1136,7 @@ _achievements: _login3: title: "Sơ cấp I" description: "Tổng số ngày đăng nhập đạt 3 ngày" - flavor: "Từ nay các bạn cứ xem như mình là một Cherrypikist đó" + flavor: "Từ nay các bạn cứ xem như mình là một Misskist đó" _login7: title: "Sơ cấp II" description: "Tổng số ngày đăng nhập đạt 7 ngày" @@ -1299,7 +1218,7 @@ _achievements: _viewAchievements3min: title: "Yêu Thành tích" description: "Ngắm danh sách thành tích đến tận hơn 3 phút" - _iLoveCherryPick: + _iLoveMisskey: title: "Tôi Yêu CherryPick" description: "Đăng lời nói \"I ❤ #CherryPick\"" flavor: "Xin chân thành cảm ơn bạn đã sử dụng CherryPick!! by Đội ngũ phát triển" @@ -1373,7 +1292,6 @@ _role: ltlAvailable: "Xem Timeline trong máy chủ này" canPublicNote: "Cho phép đăng bài công khai" canManageCustomEmojis: "Quản lý CustomEmoji" - canManageAvatarDecorations: "Quản lý trang trí ảnh đại diện" driveCapacity: "Dữ liệu Drive" pinMax: "Giới hạn ghim bài viết" antennaMax: "Giới hạn tạo ăng ten" @@ -1647,6 +1565,8 @@ _sfx: notification: "Thông báo" chat: "Trò chuyện" chatBg: "Chat (Nền)" + antenna: "Trạm phát sóng" + channel: "Kênh" _ago: future: "Tương lai" justNow: "Vừa xong" @@ -1668,6 +1588,7 @@ _2fa: registerTOTP: "Đăng ký ứng dụng xác thực" step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn." step2: "Sau đó, quét mã QR hiển thị trên màn hình này." + step2Click: "Quét mã QR trên ứng dụng xác thực (Authy, Google authenticator, v.v.)" step3Title: "Nhập mã xác thực" step3: "Nhập mã token do ứng dụng của bạn cung cấp để hoàn tất thiết lập." step4: "Kể từ bây giờ, những lần đăng nhập trong tương lai sẽ yêu cầu mã token đăng nhập đó." @@ -1834,7 +1755,6 @@ _profile: _exportOrImport: allNotes: "Toàn bộ tút" favoritedNotes: "Bài viết đã thích" - clips: "Lưu bài viết" followingList: "Đang theo dõi" muteList: "Ẩn" blockingList: "Chặn" @@ -1952,7 +1872,7 @@ _notification: yourFollowRequestAccepted: "Yêu cầu theo dõi của bạn đã được chấp nhận" youWereInvitedToGroup: "Bạn đã được mời tham gia nhóm" pollEnded: "Cuộc bình chọn đã kết thúc" - unreadAntennaNote: "Ăng ten {name}" + unreadAntennaNote: "Ăng ten" emptyPushNotificationMessage: "Đã cập nhật thông báo đẩy" achievementEarned: "Hoàn thành Achievement" _types: @@ -2007,17 +1927,11 @@ _webhookSettings: createWebhook: "Tạo Webhook" name: "Tên" secret: "Mã bí mật" + events: "Sự kiện Webhook" active: "Đã bật" _events: reaction: "Khi nhận được sự kiện" mention: "Khi có người nhắc tới bạn" -_abuseReport: - _notificationRecipient: - _recipientType: - mail: "Email" _moderationLogTypes: suspend: "Vô hiệu hóa" resetPassword: "Đặt lại mật khẩu" - createInvitation: "Tạo lời mời" -_reversi: - total: "Tổng cộng" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index d05dcd9a2c..c547b7b00d 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -11,7 +11,7 @@ password: "密码" forgotPassword: "忘记密码" fetchingAsApObject: "在联邦宇宙查询中..." ok: "OK" -gotIt: "好" +gotIt: "我明白了" cancel: "取消" noThankYou: "不用,谢谢" enterUsername: "输入用户名" @@ -58,9 +58,8 @@ copyUserId: "复制用户 ID" copyNoteId: "复制帖子 ID" copyFileId: "复制文件ID" copyFolderId: "复制文件夹ID" -copyProfileUrl: "复制个人资料URL" +copyProfileUrl: "复制配置文件URL" searchUser: "搜索用户" -searchThisUsersNotes: "搜索用户帖子" reply: "回复" loadMore: "查看更多" showMore: "查看更多" @@ -109,14 +108,11 @@ enterEmoji: "输入表情符号" renote: "转发" unrenote: "取消转发" renoted: "已转发。" -renotedToX: "转帖给 {name}" cantRenote: "该帖无法转发。" cantReRenote: "转发无法被再次转发。" quote: "引用" inChannelRenote: "在频道内转发" inChannelQuote: "在频道内引用" -renoteToChannel: "转帖至频道" -renoteToOtherChannel: "转帖至其它频道" pinnedNote: "已置顶的帖子" pinned: "置顶" you: "您" @@ -125,21 +121,14 @@ sensitive: "敏感内容" add: "添加" reaction: "回应" reactions: "回应" -emojiPicker: "表情符号选择器" -pinnedEmojisForReactionSettingDescription: "可以设置发表回应时置顶显示的表情符号" -pinnedEmojisSettingDescription: "可以设置输入表情符号时置顶显示的表情符号" -emojiPickerDisplay: "选择器显示设置" -overwriteFromPinnedEmojisForReaction: "从「置顶(回应)」设置覆盖" -overwriteFromPinnedEmojis: "从全局设置覆盖" reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。" rememberNoteVisibility: "保存上次设置的可见性" attachCancel: "删除附件" -deleteFile: "删除文件" markAsSensitive: "标记为敏感内容" unmarkAsSensitive: "取消标记为敏感内容" enterFileName: "输入文件名" mute: "屏蔽" -unmute: "解除静音" +unmute: "解除屏蔽" renoteMute: "屏蔽转帖" renoteUnmute: "解除屏蔽转帖" block: "拉黑" @@ -155,7 +144,6 @@ editList: "编辑列表" selectChannel: "选择频道" selectAntenna: "选择天线" editAntenna: "编辑天线" -createAntenna: "创建天线" selectWidget: "选择小工具" editWidgets: "编辑部件" editWidgetsExit: "完成编辑" @@ -182,10 +170,6 @@ addAccount: "添加账户" reloadAccountsList: "更新账户列表" loginFailed: "登录失败" showOnRemote: "转到所在服务器显示" -continueOnRemote: "转到所在服务器继续" -chooseServerOnMisskeyHub: "从 Misskey Hub 选择服务器" -specifyServerHost: "直接输入服务器域名" -inputHostName: "请输入域名" general: "常规设置" wallpaper: "壁纸" setWallpaper: "设置壁纸" @@ -196,7 +180,6 @@ followConfirm: "你确定要关注 {name} 吗?" proxyAccount: "代理账户" proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。" host: "主机名" -selectSelf: "选择自己" selectUser: "选择用户" recipient: "收件人" annotation: "注解" @@ -212,7 +195,6 @@ perDay: "每天" stopActivityDelivery: "停止发送活动" blockThisInstance: "阻止此服务器向本服务器推流" silenceThisInstance: "使服务器静音" -mediaSilenceThisInstance: "隐藏此服务器的媒体文件" operations: "操作" software: "软件" version: "版本" @@ -227,17 +209,15 @@ instanceInfo: "服务器信息" statistics: "统计" clearQueue: "清除队列" clearQueueConfirmTitle: "确定清除队列?" -clearQueueConfirmText: "未送达的帖子将不会被投递。 通常无需执行此操作。" +clearQueueConfirmText: "未送达的帖子将不会投递。 通常,您不需要这样做。" clearCachedFiles: "清除缓存" -clearCachedFilesConfirm: "确定要清除所有缓存的远程文件?" +clearCachedFilesConfirm: "确定要清除缓存文件?" blockedInstances: "被封锁的服务器" -blockedInstancesDescription: "设定要封锁的服务器,以换行分隔。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。" -silencedInstances: "被静音的服务器" -silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。" -mediaSilencedInstances: "已隐藏媒体文件的服务器" -mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" -muteAndBlock: "静音/拉黑" -mutedUsers: "已静音用户" +blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。" +silencedInstances: "沉默的服务器" +silencedInstancesDescription: "设置要静音的服务器的主机,以换行符分隔。属于静默服务器的所有帐户都将被视为“静默”,所有关注都将成为请求,并且您将无法提及非关注者的本地帐户。被阻止的实例不受影响。" +muteAndBlock: "屏蔽/拉黑" +mutedUsers: "已屏蔽用户" blockedUsers: "已拉黑的用户" noUsers: "无用户" editProfile: "编辑资料" @@ -280,7 +260,6 @@ removed: "已删除" removeAreYouSure: "要删掉「{x}」吗?" deleteAreYouSure: "要删掉「{x}」吗?" resetAreYouSure: "恢复默认设置?" -areYouSure: "你确定吗?" saved: "已保存" messaging: "聊天" upload: "本地上传" @@ -326,7 +305,6 @@ selectFile: "选择文件" selectFiles: "选择文件" selectFolder: "选择文件夹" selectFolders: "选择多个文件夹" -fileNotSelected: "未选择文件" renameFile: "重命名文件" folderName: "文件夹名称" createFolder: "创建文件夹" @@ -350,7 +328,7 @@ displayOfSensitiveMedia: "显示敏感媒体" whenServerDisconnected: "与服务器连接中断时" disconnectedFromServer: "已和服务器断开连接" reload: "重新加载" -doNothing: "关闭" +doNothing: "关闭弹窗" reloadConfirm: "确定要重新加载吗?" watch: "关注" unwatch: "取消关注" @@ -361,7 +339,7 @@ instanceName: "服务器名称" instanceDescription: "服务器简介" maintainerName: "管理员名称" maintainerEmail: "管理员电子邮箱" -tosUrl: "服务条款地址" +tosUrl: "服务条款 URL" thisYear: "今年" thisMonth: "本月" today: "今天" @@ -374,7 +352,7 @@ connectService: "连接" disconnectService: "断开连接" enableLocalTimeline: "启用本地时间线" enableGlobalTimeline: "启用全局时间线" -disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和监察员也可以继续使用。" +disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和协作者也可以继续使用。" registration: "注册" enableRegistration: "允许任何人注册" invite: "邀请" @@ -394,27 +372,21 @@ hcaptcha: "hCaptcha" enableHcaptcha: "启用 hCaptcha" hcaptchaSiteKey: "网站密钥" hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)" -mcaptcha: "mCaptcha" -enableMcaptcha: "启用 mCaptcha" -mcaptchaSiteKey: "网站密钥" -mcaptchaSecretKey: "mCaptcha 密钥(SecretKey)" -mcaptchaInstanceUrl: "mCaptcha 实例地址" recaptcha: "reCAPTCHA" enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" recaptchaSiteKey: "网站密钥" -recaptchaSecretKey: "mCaptcha 密钥(SecretKey)" +recaptchaSecretKey: "reCAPTCHA 密钥(SecretKey)" turnstile: "Turnstile" enableTurnstile: "启用 Turnstile" turnstileSiteKey: "网站密钥" turnstileSecretKey: "Turnstile 密钥(SecretKey)" -avoidMultiCaptchaConfirm: "使用多个 Captcha 可能会互相干扰,您要禁用其它 Captcha 吗?您可以按“取消”按钮,继续保持启用多种验证方式。" +avoidMultiCaptchaConfirm: "使用多种验证方式可能会造成干扰,您要禁用其他验证方式吗?您可以按“取消”按钮,继续保持启用多种验证方式。" antennas: "天线" manageAntennas: "天线管理" name: "名称" antennaSource: "接收来源" antennaKeywords: "包含关键字" antennaExcludeKeywords: "排除关键字" -antennaExcludeBots: "排除机器人账户" antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" notifyAntenna: "开启通知" withFileAntenna: "仅带有附件的帖子" @@ -443,8 +415,8 @@ administrator: "管理员" token: "Token (令牌)" 2fa: "双因素认证" setupOf2fa: "设置双因素认证" -totp: "验证器" -totpDescription: "使用验证器输入一次性密码" +totp: "身份验证应用" +totpDescription: "使用认证应用输入一次性密码。" moderator: "监察员" moderation: "管理" moderationNote: "管理笔记" @@ -491,12 +463,10 @@ noteOf: "{user} 的帖子" inviteToGroup: "群组邀请" quoteAttached: "已引用" quoteQuestion: "是否引用此链接内容?" -attachAsFileQuestion: "剪贴板内的文字过长。要转换为文本文件并添加吗?" noMessagesYet: "现在没有新的聊天" newMessageExists: "新信息" onlyOneFileCanBeAttached: "只能添加一个附件" signinRequired: "请先登录" -signinOrContinueOnRemote: "若要继续,需要转到您所使用的实例,或者在此服务器上注册或登录。" invitations: "邀请" invitationCode: "邀请码" checking: "正在确认" @@ -517,13 +487,12 @@ language: "语言" uiLanguage: "显示语言" groupInvited: "您有新的群组邀请" aboutX: "关于 {x}" -emojiStyle: "表情符号的样式" +emojiStyle: "emoji 的样式" native: "原生" disableDrawer: "不显示抽屉菜单" youHaveNoGroups: "没有群组" joinOrCreateGroup: "请加入一个现有的群组,或者创建新群组。" showNoteActionsOnlyHover: "仅在悬停时显示帖子操作" -showReactionsCount: "显示帖子的回应数" noHistory: "没有历史记录" signinHistory: "登录历史" enableAdvancedMfm: "启用扩展 MFM" @@ -557,7 +526,7 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐" objectStorage: "对象存储" useObjectStorage: "使用对象存储" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "用于参考的 URL,如果您正在使用 CDN 或 Proxy,请填入服务商提供的 URL;S3:“https://.s3.amazonaws.com”;GCS:“https://storage.googleapis.com/”" +objectStorageBaseUrlDesc: "这里是用于参考的 URL,如果您正在使用 CDN 或反向代理,请指定其 URL,例如 S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”" objectStorageBucket: "存储桶" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStoragePrefix: "前缀" @@ -576,7 +545,6 @@ serverLogs: "服务器日志" deleteAll: "全部删除" showFixedPostForm: "在时间线顶部显示发帖框" showFixedPostFormInChannel: "在时间线顶部显示发帖对话框(频道)" -withRepliesByDefaultForNewlyFollowed: "在时间线中默认包含新关注用户的回复" newNoteRecived: "有新的帖子" sounds: "提示音" sound: "提示音" @@ -586,8 +554,6 @@ showInPage: "在页面中显示" popout: "弹窗" volume: "音量" masterVolume: "主音量" -notUseSound: "静音" -useSoundOnlyWhenActive: "仅在 CherryPick 活跃时输出声音" details: "详情" chooseEmoji: "选择表情符号" unableToProcess: "操作无法完成" @@ -608,14 +574,10 @@ output: "输出" script: "脚本" disablePagesScript: "禁用页面脚本" updateRemoteUser: "更新远程用户信息" -unsetUserAvatar: "清除头像" -unsetUserAvatarConfirm: "要清除头像吗?" -unsetUserBanner: "清除横幅" -unsetUserBannerConfirm: "要清除横幅吗?" deleteAllFiles: "删除所有文件" deleteAllFilesConfirm: "要删除所有文件吗?" removeAllFollowing: "取消所有关注" -removeAllFollowingDescription: "取消来自 {host} 的所有关注者。当服务器不再存在时执行。" +removeAllFollowingDescription: "取消 {host} 的所有关注者。当服务器不再存在时执行。" userSuspended: "该用户已被冻结。" userSilenced: "该用户已被禁言。" yourAccountSuspendedTitle: "账户已被冻结" @@ -644,7 +606,7 @@ disablePlayer: "关闭播放器" expandTweet: "展开帖子" themeEditor: "主题编辑器" description: "描述" -describeFile: "添加描述" +describeFile: "添加标题" enterFileDescription: "输入标题" author: "作者" leaveConfirm: "存在未保存的更改。要放弃更改吗?" @@ -662,7 +624,6 @@ medium: "中" small: "小" generateAccessToken: "生成访问令牌" permission: "权限" -adminPermission: "管理员权限" enableAll: "启用全部" disableAll: "禁用全部" tokenRequested: "允许访问账户" @@ -684,7 +645,6 @@ smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" smtpSecureInfo: "使用 STARTTLS 时关闭。" testEmail: "邮件发送测试" wordMute: "文字屏蔽" -hardWordMute: "屏蔽关键词" regexpError: "正则表达式错误" regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:" instanceMute: "被屏蔽的服务器" @@ -706,7 +666,6 @@ useGlobalSettingDesc: "启用时,将使用账户通知设置。关闭时,则 other: "其他" regenerateLoginToken: "重新生成登录令牌" regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。" -theKeywordWhenSearchingForCustomEmoji: "这将是搜素自定义表情符号时的关键词。" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多个项目。" fileIdOrUrl: "文件 ID 或者 URL" behavior: "行为" @@ -860,7 +819,6 @@ administration: "管理" accounts: "账户" switch: "切换" noMaintainerInformationWarning: "管理人员信息未设置。" -noInquiryUrlWarning: "尚未设置联络地址。" noBotProtectionWarning: "Bot 防御未设置。" configure: "设置" postToGallery: "发送到图库" @@ -872,7 +830,7 @@ shareWithNote: "在帖子中分享" ads: "广告" expiration: "截止时间" startingperiod: "开始时间" -memo: "备注" +memo: "便笺" priority: "优先级" high: "高" middle: "中" @@ -920,8 +878,6 @@ makeReactionsPublicDescription: "将您发表过的回应设置成公开可见 classic: "经典" muteThread: "屏蔽帖子列表" unmuteThread: "取消屏蔽帖子列表" -followingVisibility: "关注的人的公开范围" -followersVisibility: "关注者的公开范围" continueThread: "查看更多帖子" deleteAccountConfirm: "将要删除账户。是否确认?" incorrectPassword: "密码错误" @@ -1007,7 +963,7 @@ unsubscribePushNotification: "停用推送通知消息" pushNotificationAlreadySubscribed: "推送通知消息已启用" pushNotificationNotSupported: "浏览器或服务器不支持推送通知消息" sendPushNotificationReadMessage: "删除已读推送通知消息" -sendPushNotificationReadMessageCaption: "您终端设备的电池消耗可能会增加。" +sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的通知消息将会显示。您终端设备的电池消耗可能会增加。" windowMaximize: "最大化" windowMinimize: "最小化" windowRestore: "还原" @@ -1024,7 +980,6 @@ neverShow: "不再显示" remindMeLater: "稍后提醒我" didYouLikeMisskey: "您喜欢 CherryPick 吗?" pleaseDonate: "CherryPick 是 {host} 所使用的免费软件。为了今后也能够维持 CherryPick 的开发,请在有余力的情况下进行捐助!" -correspondingSourceIsAvailable: "对应的源代码可在{anchor}找到" roles: "角色" role: "角色" noRole: "角色不存在" @@ -1034,7 +989,6 @@ assign: "分配" unassign: "取消分配" color: "颜色" manageCustomEmojis: "管理自定义表情符号" -manageAvatarDecorations: "管理头像挂件" youCannotCreateAnymore: "抱歉,您无法再创建更多了。" cannotPerformTemporary: "暂时不可用" cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,请稍后再试。" @@ -1052,7 +1006,6 @@ thisPostMayBeAnnoyingHome: "发到首页" thisPostMayBeAnnoyingCancel: "取消" thisPostMayBeAnnoyingIgnore: "就这样发布" collapseRenotes: "省略显示已经看过的转发内容" -collapseRenotesDescription: "将回应过或转贴过的贴子折叠表示。" internalServerError: "内部服务器错误" internalServerErrorDescription: "内部服务器发生了预期外的错误" copyErrorInfo: "复制错误信息" @@ -1076,11 +1029,6 @@ resetPasswordConfirm: "确定重置密码?" sensitiveWords: "敏感词" sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" -prohibitedWords: "禁用词" -prohibitedWordsDescription: "发布包含设定词汇的帖子时将出错。可用换行设定多个关键字" -prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" -hiddenTags: "隐藏标签" -hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。" notesSearchNotAvailable: "帖子检索不可用" license: "许可信息" unfavoriteConfirm: "确定要取消收藏吗?" @@ -1093,12 +1041,9 @@ enableChartsForRemoteUser: "生成远程用户的图表" enableChartsForFederatedInstances: "生成远程服务器的图表" showClipButtonInNoteFooter: "在贴文下方显示便签按钮" reactionsDisplaySize: "回应显示大小" -limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示" noteIdOrUrl: "帖子 ID 或 URL" video: "视频" videos: "视频" -audio: "音频" -audioFiles: "音频" dataSaver: "省流量模式" accountMigration: "账户迁移" accountMoved: "此用户已迁移账户" @@ -1126,8 +1071,6 @@ preservedUsernames: "保留的用户名" preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。" createNoteFromTheFile: "从文件创建帖子" archive: "归档" -archived: "已归档" -unarchive: "取消归档" channelArchiveConfirmTitle: "要将 {name} 归档吗?" channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。" thisChannelArchived: "该频道已被归档。" @@ -1138,9 +1081,6 @@ preventAiLearning: "拒绝接受生成式 AI 的学习" preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" options: "选项" specifyUser: "用户指定" -lookupConfirm: "确定查询?" -openTagPageConfirm: "确定打开话题标签页面?" -specifyHost: "指定主机名" failedToPreviewUrl: "无法预览" update: "更新" rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色" @@ -1156,7 +1096,7 @@ branding: "品牌" enableServerMachineStats: "公开服务器硬件统计信息" enableIdenticonGeneration: "启用生成用户 Identicon" turnOffToImprovePerformance: "关闭该选项可以提高性能。" -createInviteCode: "生成邀请码" +createInviteCode: "发行邀请码" createWithOptions: "使用选项来创建" createCount: "发行数" inviteCodeCreated: "已创建邀请码" @@ -1168,7 +1108,7 @@ noExpirationDate: "不设置有效日期" inviteCodeUsedAt: "邀请码被使用的日期和时间" registeredUserUsingInviteCode: "使用了邀请码的用户" waitingForMailAuth: "等待验证电子邮件" -inviteCodeCreator: "生成邀请码的用户" +inviteCodeCreator: "发行邀请码的用户" usedAt: "使用时间" unused: "未使用" used: "已使用" @@ -1199,121 +1139,20 @@ showRenotes: "显示转帖" edited: "已编辑" notificationRecieveConfig: "通知接收设置" mutualFollow: "互相关注" -followingOrFollower: "关注中或关注者" fileAttachedOnly: "仅限媒体" -showRepliesToOthersInTimeline: "在时间线中包含给别人的回复" -hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复" -showRepliesToOthersInTimelineAll: "在时间线中包含现在关注的所有人的回复" -hideRepliesToOthersInTimelineAll: "在时间线中隐藏现在关注的所有人的回复" -confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?" -confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?" -externalServices: "外部服务" -sourceCode: "源代码" -sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。" -repositoryUrl: "仓库地址" -repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若并未追加或者修改 CherryPick 的代码,请填入 https://github.com/kokonect-link/cherrypick。" -repositoryUrlOrTarballRequired: "若仓库并未公开,则需要提供 tarball 作为替代。详情请看 .config/example.yml。" -feedback: "反馈" -feedbackUrl: "反馈地址" -impressum: "运营商信息" -impressumUrl: "运营商信息地址" -impressumDescription: "德国等国家和地区有义务展示此类信息(Impressum)。" -privacyPolicy: "隐私政策" -privacyPolicyUrl: "隐私政策地址" -tosAndPrivacyPolicy: "服务条款及隐私政策" +showRepliesToOthersInTimeline: "在时间线上显示给其他人的回复" +hideRepliesToOthersInTimeline: "在时间线上隐藏给其他人的回复" avatarDecorations: "头像挂件" -attach: "佩戴" -detach: "卸下" -detachAll: "全部卸下" -angle: "角度" flip: "翻转" -showAvatarDecorations: "显示头像挂件" -releaseToRefresh: "松开以刷新" -refreshing: "刷新中" -pullDownToRefresh: "下拉以刷新" -disableStreamingTimeline: "禁止实时更新时间线" -useGroupedNotifications: "分组显示通知" -signupPendingError: "确认电子邮件时出现错误。链接可能已过期。" -cwNotationRequired: "在启用「隐藏内容」时必须输入注释" -doReaction: "回应" -code: "代码" -reloadRequiredToApplySettings: "需要重新载入来使设置生效" -remainingN: "剩余:{n}" -overwriteContentConfirm: "将覆盖现有内容。确定吗?" -seasonalScreenEffect: "符合当前季节的画面效果" -decorate: "装饰" -addMfmFunction: "添加装饰" -enableQuickAddMfmFunction: "显示高级 MFM 选择器" -bubbleGame: "泡泡游戏" -sfx: "音效" -soundWillBePlayed: "声音将会播放" -showReplay: "观看回放" -replay: "重播" -replaying: "重播中" -endReplay: "结束回放" -copyReplayData: "复制回放数据" -ranking: "排行榜" -lastNDays: "最近 {n} 天" -backToTitle: "返回标题" -hemisphere: "居住地区" -withSensitive: "显示包含敏感媒体的帖子" -userSaysSomethingSensitive: "含 {name} 敏感文件的帖子" -enableHorizontalSwipe: "滑动切换标签页" -loading: "读取中" -surrender: "取消" -gameRetry: "重试" -notUsePleaseLeaveBlank: "如不使用请留空" -useTotp: "使用一次性代码" -useBackupCode: "使用备用代码" -launchApp: "启动应用" -useNativeUIForVideoAudioPlayer: "使用浏览器的 UI 播放动画及音频" -keepOriginalFilename: "保持原文件名" -keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名将被替换为随机字符。" -noDescription: "没有描述" -alwaysConfirmFollow: "总是确认关注" -inquiry: "联系我们" -tryAgain: "请再试一次" -confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认" -sensitiveMediaRevealConfirm: "这是敏感内容。是否显示?" -createdLists: "已创建的列表" -createdAntennas: "已创建的天线" -_delivery: - status: "投递状态" - stop: "停止投递" - resume: "继续投递" - _type: - none: "投递中" - manuallySuspended: "手动停止中" - goneSuspended: "因服务器被删除而停止" - autoSuspendedForNotResponding: "因服务器无应答而停止" -_bubbleGame: - howToPlay: "游戏说明" - hold: "抓住" - _score: - score: "得分" - scoreYen: "赚到的钱" - highScore: "最高分" - maxChain: "最高连击数" - yen: "{yen} 日元" - estimatedQty: "约 {qty} 个" - scoreSweets: "相当于 {onigiriQtyWithUnit} 饭团" - _howToPlay: - section1: "对准位置将Emoji投入盒子。" - section2: "相同的Emoji相互接触合成后会得到新的Emoji,以此获得分数。" - section3: "如果Emoji从箱子中溢出游戏将会结束。在防止Emoji溢出的同时,不断合成新的Emoji,来获取更高的分数吧!" _announcement: forExistingUsers: "仅限现有用户" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" needConfirmationToRead: "需要确认才能标记为已读" needConfirmationToReadDescription: "若启用,则会在标记已读时会显示确认对话框。此外,它也会不受批量已读操作的影响。" end: "结束公告" - tooManyActiveAnnouncementDescription: "若有大量活动公告,可能会造成用户体验下降。请考虑归档已完成的公告。" + tooManyActiveAnnouncementDescription: "若有大量活动公告,可能会造成用户体验可能下降。请考虑归档已完成的公告。" readConfirmTitle: "标记为已读?" readConfirmText: "阅读“{title}”的内容并将其标记为已读。" - shouldNotBeUsedToPresentPermanentInfo: "我们建议使用公告来发布临时性的流动信息而不是固定的常规信息,因为这可能损害用户体验,尤其是对于新用户而言。" - dialogAnnouncementUxWarn: "同时存在 2 个或以上的对话框公告极有可能对用户体验产生负面的影响,建议谨慎使用。" - silence: "不发送通知" - silenceDescription: "开启后,此条公告将不会发送通知,也不强制用户阅读。" _initialAccountSetting: accountCreated: "账户创建完成了!" letsStartAccountSetup: "来进行帐户的初始设置吧。" @@ -1326,97 +1165,23 @@ _initialAccountSetting: pushNotificationDescription: "启用推送通知的话,就可以在设备上接收来自 {name} 的通知了。" initialAccountSettingCompleted: "初始设定已经完成了!" haveFun: "希望 {name} 在这里玩得开心!" - youCanContinueTutorial: "您可以继续了解 {name}(CherryPick) 的使用教程,也可以在此停止教程并立即开始使用它。\n" - startTutorial: "开始教学" skipAreYouSure: "要跳过初始设置吗?" laterAreYouSure: "要稍后再进行初始设定吗?" -_initialTutorial: - launchTutorial: "观看教学" - title: "教学" - wellDone: "做得好" - skipAreYouSure: "是否退出教学?" - _landing: - title: "欢迎来到教学" - description: "在这里,您可以查看 CherryPick 的基本使用方法和功能。" - _note: - title: "什么是帖子?" - description: "在 CherryPick 上发表的文章称为「帖子」。帖子在时间线上按照时间顺序排列,并实时更新。" - reply: "用来回复帖子。可以对回复进行回复,从而形成一串对话。" - renote: "用来将帖子共享到自己的时间线上。也可以加上自己的文字然后引用它。" - reaction: "用来添加回应。详细信息将在下一页进行说明。" - menu: "用来进行例如显示帖子详情、复制链接等各种各样的操作。" - _reaction: - title: "什么是回应?" - description: "您可以在帖子中添加“回应”。 您可以使用反应轻松地表达点“赞”所无法传达的细微差别。" - letsTryReacting: "回应可以通过点击帖子中的「+」按钮来添加。试着给这个示例帖子添加一个回应!" - reactToContinue: "添加一个回应来继续" - reactNotification: "当您的帖子被某人添加了回应时,将实时收到通知。" - reactDone: "通过按下「ー」按钮,可以取消已经添加的回应" - _timeline: - title: "时间线的运作方式" - description1: "CherryPick 根据使用方式提供了多个时间线(根据服务器的设定,可能有一些被禁用)。" - home: "可以查看您关注的账户的帖子。" - local: "可以查看这个服务器上所有用户发表的帖子。" - social: "将同时显示首页时间线和本地时间线的内容。" - global: "可以查看所有已联合的服务器上的帖子。" - description2: "可以随时在屏幕顶部在每个时间线之间切换。" - description3: "另外,还有列表时间线和频道时间线。请参阅{link}了解更多详细信息。" - _postNote: - title: "帖子发布设置" - description1: "在 CherryPick 发布帖子时,您可以设置各种选项。发帖窗口看起来是这样的。\n" - _visibility: - description: "您可以限制谁可以看到您的帖子。" - public: "向所有用户公开。\n" - home: "仅在首页时间线上发布。 关注者、从个人资料页查看过来的用户、以及通过转帖也能被别的用户看见。" - followers: "仅对关注者可见。 除了您自己之外,没有人可以转贴,并且只有您的关注者可以查看它。\n" - direct: "它将仅向指定用户公开,并且他们也会收到通知。 您可以使用它来代替私信。\n" - doNotSendConfidencialOnDirect1: "发送敏感信息时请注意。\n" - doNotSendConfidencialOnDirect2: "目标服务器的管理员可以看到发布的内容,因此如果您向不受信任的服务器上的用户发送私信,则在处理敏感信息时需要小心。" - localOnly: "不将帖子推送到其它服务器。 无论上述公开范围如何,其它服务器的用户将无法看到附加了此设定的帖子。\n" - _cw: - title: "隐藏内容 (CW)\n" - description: "显示「注解」里的内容而不是正文。点击「查看更多」将会把正文显示出来。" - _exampleNote: - cw: "深夜报复社会" - note: "茨了带巧克力的甜甜圈🍩😋" - useCases: "用于服务器条款所规定的帖子,或对剧透内容和敏感内容进行自主规制。" - _howToMakeAttachmentsSensitive: - title: "如何将附件标注为敏感内容?" - description: "对于服务器方针所要求要求的,又或者不适合直接展示的附件,请添加「敏感」标记。\n" - tryThisFile: "试试看,将附加到此窗口的图像标注为敏感!" - _exampleNote: - note: "拆纳豆包装时出错了…" - method: "要标注附件为敏感内容,请单击该文件以打开菜单,然后单击“标记为敏感内容”。" - sensitiveSucceeded: "附加文件时,请遵循服务器的条款来设置正确敏感设定。\n" - doItToContinue: "将图像标记为敏感后才能够继续" - _done: - title: "恭喜您,已经完成了教程🎉\n" - description: "这里介绍的只是其中一小部分的功能。 要了解更多有关如何使用 CherryPick 的更多信息,请访问 {link}。" -_timelineDescription: - home: "首页时间线可以查看您关注的账户的帖子。" - local: "本地时间线可以查看这个服务器上所有用户发表的帖子。" - social: "社交时间线将同时显示首页时间线和本地时间线的内容。" - global: "全局时间线可以查看所有已联合的服务器上的帖子。" _serverRules: description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。" _serverSettings: iconUrl: "图标 URL" appIconDescription: "指定当 {host} 显示为 app 时的图标。" - appIconUsageExample: "如作为书签添加到 PWA 或手机主屏幕时" + appIconUsageExample: "例如:作为书签添加到 PWA 或手机主屏幕的时候" appIconStyleRecommendation: "因为有可能会被裁切为圆形或者圆角矩形,建议使用边缘带有留白背景的图标。" appIconResolutionMustBe: "分辨率必须为 {resolution}。" manifestJsonOverride: "覆盖 manifest.json" shortName: "简称" shortNameDescription: "如果服务器的正式名称很长,可以用简称或者別名来替代。" - fanoutTimelineDescription: "当启用时,可显著提高获取各种时间线时的性能,并减轻数据库的负荷。但是相对的 Redis 的内存使用量将会增加。如果服务器的内存不是很大,又或者运行不稳定的话可以把它关掉。" - fanoutTimelineDbFallback: "回退到数据库" - fanoutTimelineDbFallbackDescription: "当启用时,若时间线未被缓存,则将额外查询数据库。禁用该功能可通过不执行回退处理进一步减少服务器负载,但会限制可检索的时间线范围。" - inquiryUrl: "联络地址" - inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。" _accountMigration: moveFrom: "从别的账号迁移到此账户" moveFromSub: "为另一个账户建立别名" - moveFromLabel: "迁移前的账户 #{n}" + moveFromLabel: "迁移前的账户" moveFromDescription: "如果迁移时需要继承其他账户的关注者,你需要创建一个别名。此操作需要在迁移前完成!\n请像这样输入要迁移的账户:@username@server.example.com\n如果要删除,请将输入字段留空,并保存(不推荐)。" moveTo: "把这个账户迁移到新的账户" moveToLabel: "迁移后的账户" @@ -1484,7 +1249,7 @@ _achievements: _login3: title: "初学者 I" description: "累计登录 3 天" - flavor: "今天开始我就是 Cherrypikist!" + flavor: "今天开始我就是 Misskist!" _login7: title: "初学者 II" description: "累计登录 7 天" @@ -1589,7 +1354,7 @@ _achievements: _viewAchievements3min: title: "成就爱好者" description: "盯着成就看三分钟" - _iLoveCherryPick: + _iLoveMisskey: title: "I Love CherryPick" description: "发布 \"I ❤ #CherryPick\" 帖子" flavor: "感谢您使用 CherryPick ! by 开发团队" @@ -1639,7 +1404,7 @@ _achievements: description: "点了这里" _justPlainLucky: title: "超高校级的幸运" - description: "每 10 秒有 0.005% 的概率自动获得" + description: "每 10 秒有 0.01 的概率自动获得" _setNameToSyuilo: title: "像神一样呐" description: "将名称设定为 syuilo" @@ -1673,15 +1438,6 @@ _achievements: _smashTestNotificationButton: title: "过度测试" description: "短时间内连续测试通知" - _tutorialCompleted: - title: "CherryPick 初学者课程 结业证书" - description: "完成了教学" - _bubbleGameExplodingHead: - title: "🤯" - description: "你合成出了游戏里最大的Emoji" - _bubbleGameDoubleExplodingHead: - title: "两个🤯" - description: "你合成出了2个游戏里最大的Emoji" _role: new: "创建角色" edit: "编辑角色" @@ -1692,9 +1448,7 @@ _role: assignTarget: "授权对象" descriptionOfAssignTarget: "手动指手动选择谁被包括在这个角色中。\n符合条件指设置条件以自动包括符合条件的用户。" manual: "手动" - manualRoles: "手动角色" conditional: "符合条件" - conditionalRoles: "条件角色" condition: "条件" isConditionalRole: "这是一个条件控制的角色。" isPublic: "角色公开" @@ -1711,8 +1465,8 @@ _role: descriptionOfIsExplorable: "打开后将公开角色时间线。如果角色不是公开的,就无法公开时间线。" displayOrder: "显示顺序" descriptionOfDisplayOrder: "数字越大,显示位置越靠前。" - canEditMembersByModerator: "允许监察员编辑成员" - descriptionOfCanEditMembersByModerator: "如果选中,监察员和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" + canEditMembersByModerator: "允许监察者编辑成员" + descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" priority: "优先级" _priority: low: "低" @@ -1723,16 +1477,13 @@ _role: ltlAvailable: "查看本地时间线" canPublicNote: "允许公开发帖" canEditNote: "编辑帖子" - mentionMax: "帖子内最多提及数" canInvite: "发放服务器邀请码" - inviteLimit: "可生成邀请码的数量" + inviteLimit: "可发行邀请码的数量" inviteLimitCycle: "邀请码的发行间隔" inviteExpirationTime: "邀请码的有效日期" canManageCustomEmojis: "管理自定义表情符号" - canManageAvatarDecorations: "管理头像挂件" driveCapacity: "网盘容量" alwaysMarkNsfw: "总是将文件标记为 NSFW" - canUpdateBioMedia: "可以更新头像和横幅" pinMax: "帖子置顶数量限制" antennaMax: "可创建的最大天线数量" wordMuteMax: "屏蔽词的字数限制" @@ -1745,17 +1496,9 @@ _role: descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" canHideAds: "可以隐藏广告" canSearchNotes: "是否可以搜索帖子" - canUseTranslator: "使用翻译功能" - avatarDecorationLimit: "可添加头像挂件的最大个数" _condition: - roleAssignedTo: "已分配给手动角色" isLocal: "是本地用户" isRemote: "是远程用户" - isCat: "猫猫用户" - isBot: "机器人用户" - isSuspended: "停用的用户" - isLocked: "锁推用户" - isExplorable: "启用“使账号可见”的用户" createdLessThan: "账户创建时间少于" createdMoreThan: "账户创建时间超过" followersLessThanOrEq: "关注者不多于" @@ -1781,7 +1524,6 @@ _emailUnavailable: disposable: "不是永久可用的地址" mx: "邮件服务器不正确" smtp: "邮件服务器没有响应" - banned: "无法使用此邮件地址注册" _ffVisibility: public: "公开" followers: "只有关注你的用户能看到" @@ -1802,10 +1544,6 @@ _ad: reduceFrequencyOfThisAd: "减少此广告的频率" hide: "不显示" timezoneinfo: "星期几是由服务器的时区所指定的。" - adsSettings: "广告设置" - notesPerOneAd: "在实时更新时间线中插入广告的间隔(帖子个数)" - setZeroToDisable: "设为 0 将不在实时更新时间线中投放广告" - adsTooClose: "广告投放时间间隔过短将可能显著损害用户体验。" _forgotPassword: enterEmail: "请输入您设置的电子邮箱地址,密码重置链接将发送至该邮箱上。" ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。" @@ -1825,7 +1563,6 @@ _plugin: installWarn: "请不要安装不可信的插件。" manage: "管理插件..." viewSource: "查看源代码" - viewLog: "显示日志" _preferencesBackups: list: "已创建的备份" saveNew: "另存为" @@ -1846,8 +1583,8 @@ _preferencesBackups: invalidFile: "无效的的文件格式。" _registry: scope: "范围" - key: "键" - keys: "键" + key: "主要" + keys: "主要" domain: "域" createKey: "创建键" _aboutMisskey: @@ -1855,13 +1592,10 @@ _aboutMisskey: contributors: "主要贡献者" allContributors: "全体贡献者" source: "源代码" - original: "原版" - thisIsModifiedVersion: "{name}正在使用修改后的 CherryPick。" translation: "翻译 Misskey" donate: "赞助 Misskey" morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰" patrons: "支持者" - projectMembers: "项目成员" _displayOfSensitiveMedia: respect: "隐藏敏感媒体" ignore: "显示敏感媒体" @@ -1952,7 +1686,6 @@ _channel: notesCount: "有 {n} 个帖子" nameAndDescription: "名称与描述" nameOnly: "仅名称" - allowRenoteToExternal: "允许在频道外转帖及引用" _menuDisplay: sideFull: "横向" sideIcon: "横向(图标)" @@ -2044,15 +1777,8 @@ _sfx: notification: "通知" chat: "聊天" chatBg: "聊天背景" - reaction: "选择回应时" -_soundSettings: - driveFile: "使用网盘内的音频" - driveFileWarn: "选择网盘上的文件" - driveFileTypeWarn: "不支持此文件" - driveFileTypeWarnDescription: "请选择音频文件" - driveFileDurationWarn: "音频过长" - driveFileDurationWarnDescription: "使用长音频可能会影响 CherryPick 的使用。即使这样也要继续吗?" - driveFileError: "无法读取声音。请更改设置。" + antenna: "天线接收" + channel: "频道通知" _ago: future: "未来" justNow: "最近" @@ -2066,12 +1792,7 @@ _ago: invalid: "没有" _timeIn: seconds: "{n}秒后" - minutes: "{n} 分后" - hours: "{n} 小时后" days: "{n}天后" - weeks: "{n} 周后" - months: "{n} 月后" - years: "{n} 年后" _time: second: "秒" minute: "分" @@ -2079,33 +1800,33 @@ _time: day: "日" _2fa: alreadyRegistered: "此设备已被注册" - registerTOTP: "开始设置验证器" + registerTOTP: "开始设置认证应用" step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。" step2: "然后,扫描屏幕上显示的二维码。" + step2Click: "通过点击二维码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册" step2Uri: "如果使用桌面应用程序的话,请输入下面的 URI" step3Title: "输入验证码" step3: "输入您的应用提供的动态口令以完成设置。" setupCompleted: "设置完成" step4: "从现在开始,任何登录操作都将要求您提供动态口令。" securityKeyNotSupported: "您的浏览器不支持安全密钥。" - registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器。" + registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器应用程序。" securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。" registerSecurityKey: "注册安全密钥或 Passkey" securityKeyName: "输入密钥名称" tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或 Passkey。" removeKey: "删除安全密钥" removeKeyConfirm: "您确定要删除 {name} 吗?" - whyTOTPOnlyRenew: "当注册了安全密钥时,无法取消使用验证器。" - renewTOTP: "重置验证器" - renewTOTPConfirm: "当前验证器的验证码及备用代码已失效" + whyTOTPOnlyRenew: "如果注册了安全密钥,则无法取消验证器应用程序上的设置。" + renewTOTP: "重置验证器应用程序" + renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效" renewTOTPOk: "重新配置" renewTOTPCancel: "不用,谢谢" checkBackupCodesBeforeCloseThisWizard: "在关闭此窗口前,请确认下面的备用代码" backupCodes: "备用代码" - backupCodesDescription: "如果无法使用验证器,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。" - backupCodeUsedWarning: "已使用备用代码。若验证器无法使用,请尽快重置验证器。" - backupCodesExhaustedWarning: "已使用完所有的备用代码。若验证器无法使用,则无法再访问您的账户。请重置验证器。" - moreDetailedGuideHere: "此处为详细指南" + backupCodesDescription: "如果无法使用认证应用,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。" + backupCodeUsedWarning: "已使用备用代码。如果无法使用认证应用,请尽快重新设定。" + backupCodesExhaustedWarning: "已使用完所有的备用代码。如果无法使用认证应用,将无法再访问您的账户。请再次设定认证应用。" _permissions: "read:account": "查看账户信息" "write:account": "更改帐户信息" @@ -2143,54 +1864,6 @@ _permissions: "write:flash": "编辑 Play" "read:flash-likes": "查看 Play 的点赞" "write:flash-likes": "编辑 Play 的点赞列表" - "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": "查看用户 IP 地址" - "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: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": "查看用户网盘相关情报" - "read:admin:stream": "使用管理员用的 Websocket API" - "write:admin:ad": "编辑广告" - "read:admin:ad": "查看广告" - "write:invite-codes": "生成邀请码" - "read:invite-codes": "获取已发行的邀请码" - "write:clip-favorite": "编辑便签的点赞" - "read:clip-favorite": "查看便签的点赞" - "read:federation": "查看联合相关信息" - "write:report-abuse": "举报用户" _auth: shareAccessTitle: "应用程序授权许可" shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?" @@ -2246,7 +1919,6 @@ _widgets: _userList: chooseList: "选择列表" clicker: "点击器" - birthdayFollowings: "今天是他们的生日" _cw: hide: "隐藏" show: "查看更多" @@ -2309,18 +1981,15 @@ _profile: changeAvatar: "修改头像" changeBanner: "修改横幅" verifiedLinkDescription: "如果将内容设置为 URL,当链接所指向的网页内包含自己的个人资料链接时,可以显示一个已验证图标。" - avatarDecorationMax: "最多可添加 {max} 个挂件" _exportOrImport: allNotes: "所有帖子" favoritedNotes: "收藏的帖子" - clips: "便签" followingList: "关注中" muteList: "屏蔽" blockingList: "拉黑" userLists: "列表" excludeMutingUsers: "排除屏蔽用户" excludeInactiveUsers: "排除不活跃用户" - withReplies: "在时间线中包含导入用户的回复" _charts: federation: "联合" apRequest: "请求" @@ -2368,7 +2037,6 @@ _play: title: "标题" script: "脚本" summary: "描述" - visibilityDescription: "设置为不公开后资料将不再显示,但知道 URL 的人仍可继续访问。" _pages: newPage: "创建页面" editPage: "编辑页面" @@ -2413,8 +2081,6 @@ _pages: section: "章节" image: "图片" button: "按钮" - dynamic: "动态区块" - dynamicDescription: "这个区块已经废弃。以后请使用{play}。" note: "嵌入的帖子" _note: id: "帖子 ID" @@ -2437,18 +2103,12 @@ _notification: pollEnded: "问卷调查结果已生成。" newNote: "新的帖子" unreadAntennaNote: "天线 {name}" - roleAssigned: "授予的角色" emptyPushNotificationMessage: "推送通知已更新" achievementEarned: "获得成就" testNotification: "测试通知" checkNotificationBehavior: "检查通知显示" sendTestNotification: "发送测试通知" notificationWillBeDisplayedLikeThis: "通知将会这样表示" - reactedBySomeUsers: "{n} 人回应了" - likedBySomeUsers: "{n}人赞了你的帖子" - renotedBySomeUsers: "{n} 人转发了" - followedBySomeUsers: "被 {n} 人关注" - flushNotification: "重置通知历史" _types: all: "全部" note: "用户的新帖子" @@ -2462,7 +2122,6 @@ _notification: receiveFollowRequest: "收到关注请求" followRequestAccepted: "关注请求已通过" groupInvited: "加入群组邀请" - roleAssigned: "授予的角色" achievementEarned: "取得的成就" app: "关联应用的通知" _actions: @@ -2473,7 +2132,6 @@ _deck: alwaysShowMainColumn: "总是显示主列" columnAlign: "列对齐" addColumn: "添加列" - newNoteNotificationSettings: "新帖子通知设定" configureColumn: "列设置" swapLeft: "向左移动" swapRight: "向右移动" @@ -2512,10 +2170,9 @@ _drivecleaner: orderByCreatedAtAsc: "按添加日期降序排列" _webhookSettings: createWebhook: "创建 Webhook" - modifyWebhook: "编辑 webhook" name: "名称" secret: "密钥" - trigger: "触发器" + events: "何时运行 Webhook" active: "已启用" _events: follow: "关注时" @@ -2525,26 +2182,6 @@ _webhookSettings: renote: "被转发时" reaction: "被回应时" mention: "被提及时" - _systemEvents: - abuseReport: "当收到举报时" - abuseReportResolved: "当举报被处理时" - userCreated: "当用户被创建时" - deleteConfirm: "要删除 webhook 吗?" -_abuseReport: - _notificationRecipient: - createRecipient: "新建举报通知" - modifyRecipient: "编辑举报通知" - recipientType: "通知类型" - _recipientType: - mail: "邮箱" - webhook: "Webhook" - _captions: - mail: "当收到新举报时,向持有监察员权限的用户发送通知邮件" - webhook: "当收到新举报及举报被处理时,使用指定的 SystemWebhook 发送通知" - keywords: "关键字" - notifiedUser: "通知的用户" - notifiedWebhook: "使用的 webhook" - deleteConfirm: "要删除通知吗?" _moderationLogTypes: createRole: "创建角色" deleteRole: "删除角色" @@ -2567,160 +2204,17 @@ _moderationLogTypes: deleteGlobalAnnouncement: "删除全体通知" deleteUserAnnouncement: "删除用户通知" resetPassword: "重置密码" - suspendRemoteInstance: "停止远程服务器" - unsuspendRemoteInstance: "恢复远程服务器" - updateRemoteInstanceNote: "更新远程服务器的管理笔记" markSensitiveDriveFile: "标记网盘文件为敏感媒体" unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体" resolveAbuseReport: "处理举报" - createInvitation: "生成邀请码" + createInvitation: "发行邀请码" createAd: "创建了广告" deleteAd: "删除了广告" updateAd: "更新了广告" - createAvatarDecoration: "新建头像挂件" - updateAvatarDecoration: "更新头像挂件" - deleteAvatarDecoration: "删除头像挂件" - unsetUserAvatar: "清除用户头像" - unsetUserBanner: "清除用户横幅" - createSystemWebhook: "新建了 SystemWebhook" - updateSystemWebhook: "更新了 SystemWebhook" - deleteSystemWebhook: "删除了 SystemWebhook" - createAbuseReportNotificationRecipient: "新建了举报通知" - updateAbuseReportNotificationRecipient: "更新了举报通知" - deleteAbuseReportNotificationRecipient: "删除了举报通知" _fileViewer: - title: "文件信息" - type: "文件类型" - size: "文件大小" url: "URL" uploadedAt: "添加日期" - attachedNotes: "附加到的帖子" - thisPageCanBeSeenFromTheAuthor: "此页只能被该文件的上传者查看。" _externalResourceInstaller: - title: "从外部站点安装" - checkVendorBeforeInstall: "请在安装前确保来源可靠" - _plugin: - title: "要安装此插件吗?" - metaTitle: "插件信息" - _theme: - title: "要安装此主题吗?" - metaTitle: "主题信息" - _meta: - base: "基本配色方案" - _vendorInfo: - title: "来源信息" - endpoint: "参考端点" - hashVerify: "确认文件完整性" _errors: - _invalidParams: - title: "缺少参数" - description: "缺少从外部站点获取数据所需的信息。请检查 URL。" - _resourceTypeNotSupported: - title: "不支持此外部资源" - description: "不支持从此外部站点获取的资源类型。请联系站点管理员。" - _failedToFetch: - title: "获取数据失败" - fetchErrorDescription: "与外部站点的通信失败。 如果重试后问题仍然存在,请联系站点管理员。" - parseErrorDescription: "无法读取从外部站点取得的数据。请联系站点管理员。" - _hashUnmatched: - title: "无法获取正确数据" - description: "无法验证数据的完整性。安全起见,无法继续安装。请联系站点管理员。" _pluginParseFailed: title: "AiScript 错误" - description: "虽然取得了数据,但是由于 AiScript 解析时出现错误,无法读取数据。请联系插件的作者。可在 Javascript 控制台查看错误详情。" - _pluginInstallFailed: - title: "插件安装失败" - description: "安装插件时出现错误。请再试一次。可在 Javascript 控制台查看错误详情。" - _themeParseFailed: - title: "主题解析错误" - description: "虽然取得了主题文件,但是由于解析时出现错误,无法加载主题。请联系主题的作者。可在 Javascript 控制台查看错误详情。" - _themeInstallFailed: - title: "安装主题失败" - description: "安装主题时出错。请再试一次。可在 Javascript 控制台查看错误详情。" -_dataSaver: - _media: - title: "加载媒体" - description: "防止自动加载图像和视频。 点击隐藏的图像/视频即可加载它们。\n" - _avatar: - title: "头像" - description: "停止播放头像的动画。 由于动画图片的文件大小可能比普通图像大,这可以进一步减少数据流量。" - _urlPreview: - title: "URL预览缩略图\n" - description: "将不再加载 URL 预览缩略图。" - _code: - title: "代码高亮" - description: "如果使用了代码高亮标记,例如在 MFM 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。" -_hemisphere: - N: "北半球" - S: "南半球" - caption: "在某些客户端设置中用来确定季节" -_reversi: - reversi: "黑白棋" - gameSettings: "对局设置" - chooseBoard: "选择棋盘" - blackOrWhite: "先手/后手" - blackIs: "{name}执黑(先手)" - rules: "规则" - thisGameIsStartedSoon: "对局即将开始" - waitingForOther: "等待对手准备" - waitingForMe: "等待你的准备" - waitingBoth: "请准备" - ready: "准备就绪" - cancelReady: "重新准备" - opponentTurn: "对手的回合" - myTurn: "你的回合" - turnOf: "{name}的回合" - pastTurnOf: "{name}的回合" - surrender: "认输" - surrendered: "已认输" - timeout: "超时" - drawn: "平局" - won: "{name}获胜" - black: "黑" - white: "白" - total: "总计" - turnCount: "第{count}回合" - myGames: "我的对局" - allGames: "所有对局" - ended: "结束" - playing: "对局中" - isLlotheo: "落子少的一方获胜(又名奥赛罗)" - loopedMap: "循环棋盘" - canPutEverywhere: "无限制放置模式" - timeLimitForEachTurn: "1回合的时间限制" - freeMatch: "自由匹配" - lookingForPlayer: "正在寻找对手" - gameCanceled: "对局被取消了" - shareToTlTheGameWhenStart: "开始时在时间线发布对局" - iStartedAGame: "对局开始!#MisskeyReversi" - opponentHasSettingsChanged: "对手更改了设定" - allowIrregularRules: "允许非常规规则(完全自由)" - disallowIrregularRules: "禁止非常规规则" - showBoardLabels: "显示行号和列号" - useAvatarAsStone: "用头像作为棋子" -_offlineScreen: - title: "离线——无法连接到服务器" - header: "无法连接到服务器" -_urlPreviewSetting: - title: "设置 URL 预览" - enable: "启用 URL 预览" - timeout: "超时阈值(ms)" - timeoutDescription: "如果获取预览所用时间超过这个值,则不生成预览。" - maximumContentLength: "Content-Length 的最大值(byte)" - maximumContentLengthDescription: "如果 Content-Length 超过这个值,则不生成预览。" - requireContentLength: "仅在能取得 Content-Length 时生成预览" - requireContentLengthDescription: "如果目标服务器不返回 Content-Length,则不生成预览。" - userAgent: "User-Agent" - userAgentDescription: "设定获取预览时使用的 User-Agent。留空时将使用默认的 User-Agent。" - summaryProxy: "用来生成预览的代理的 endpoint。" - summaryProxyDescription: "不使用 Misskey 本体,而是通过 Summaly Proxy 生成预览。" - summaryProxyDescription2: "下面的参数将作为查询字符串发送至代理。代理侧如果不支持此设置,则忽略设定值。" -_mediaControls: - pip: "画中画" - playbackRate: "播放速度" - loop: "循环播放" -_contextMenu: - title: "上下文菜单" - app: "应用" - appWithShift: "Shift 键应用" - native: "浏览器的用户界面" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index c6e1fdcbfd..bd1563fe6a 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -60,14 +60,13 @@ copyFileId: "複製檔案 ID" copyFolderId: "複製資料夾ID" copyProfileUrl: "複製個人資料網址" searchUser: "搜尋使用者" -searchThisUsersNotes: "搜尋這個使用者的貼文" reply: "回覆" loadMore: "載入更多" showMore: "載入更多" showLess: "關閉" youGotNewFollower: "您有新的追隨者" receiveFollowRequest: "您有新的追隨請求" -followRequestAccepted: "追隨請求已被接受" +followRequestAccepted: "追隨請求已接受" mention: "提及" mentions: "提及" directNotes: "私訊" @@ -92,7 +91,7 @@ manageLists: "管理清單" error: "錯誤" somethingHappened: "發生錯誤" retry: "重試" -pageLoadError: "無法載入頁面。" +pageLoadError: "載入頁面失敗" pageLoadErrorDescription: "這通常是網路錯誤或瀏覽器快取殘留而引起的。請先清除瀏覽器快取,稍後再重試。" serverIsDead: "伺服器沒有回應。請稍等片刻再試。" youShouldUpgradeClient: "請重新載入以使用新版客戶端顯示此頁面。" @@ -109,14 +108,11 @@ enterEmoji: "輸入表情符號" renote: "轉發" unrenote: "取消轉發" renoted: "轉發成功。" -renotedToX: "轉發給 {name} 了。" cantRenote: "無法轉發此貼文。" cantReRenote: "無法轉發之前已經轉發過的內容。" quote: "引用" inChannelRenote: "在頻道內轉發" inChannelQuote: "在頻道內引用" -renoteToChannel: "轉發至頻道" -renoteToOtherChannel: "轉發至其他頻道" pinnedNote: "已置頂的貼文" pinned: "置頂" you: "您" @@ -134,7 +130,6 @@ overwriteFromPinnedEmojis: "從一般複寫設定" reactionSettingDescription2: "拖動以交換,點擊以刪除,按下「+」以新增。" rememberNoteVisibility: "記住貼文可見性" attachCancel: "移除附件" -deleteFile: "刪除檔案" markAsSensitive: "標記為敏感內容" unmarkAsSensitive: "取消標記為敏感內容" enterFileName: "請輸入檔案名稱" @@ -155,7 +150,6 @@ editList: "編輯清單" selectChannel: "選擇頻道" selectAntenna: "選擇天線" editAntenna: "編輯天線" -createAntenna: "建立天線" selectWidget: "選擇小工具" editWidgets: "編輯小工具" editWidgetsExit: "完成" @@ -174,7 +168,7 @@ cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取 flagAsBot: "此使用者是機器人" flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整 CherryPick 內部系統將本帳戶識別為機器人。" flagAsCat: "此帳戶是一隻貓,喵~~~!!!" -flagAsCatDescription: "喵喵喵??" +flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" flagShowTimelineRepliesDescription: "啟用後,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。" autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求" @@ -182,10 +176,6 @@ addAccount: "新增帳戶" reloadAccountsList: "更新帳戶清單的資訊" loginFailed: "登入失敗" showOnRemote: "轉到所在實例顯示" -continueOnRemote: "在遠端伺服器繼續" -chooseServerOnMisskeyHub: "從 Misskey Hub 選擇伺服器" -specifyServerHost: "直接指定伺服器網域" -inputHostName: "請輸入域名" general: "一般" wallpaper: "桌布" setWallpaper: "設定桌布" @@ -196,7 +186,6 @@ followConfirm: "你真的要追隨{name}嗎?" proxyAccount: "代理帳戶" proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。" host: "主機" -selectSelf: "選擇自己" selectUser: "選取使用者" recipient: "收件人" annotation: "註解" @@ -212,11 +201,10 @@ perDay: "每日" stopActivityDelivery: "停止發送活動" blockThisInstance: "封鎖此伺服器" silenceThisInstance: "禁言此伺服器" -mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言" operations: "操作" software: "軟體" version: "版本" -metadata: "詮釋資料" +metadata: "元資料" withNFiles: "{n} 個檔案" monitor: "監視器" jobQueue: "佇列" @@ -234,8 +222,6 @@ blockedInstances: "已封鎖的伺服器" blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。" silencedInstances: "被禁言的伺服器" silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。" -mediaSilencedInstances: "媒體被禁言的伺服器" -mediaSilencedInstancesDescription: "設定您想要對媒體設定禁言的伺服器,以換行符號區隔。來自被媒體禁言的伺服器所屬帳戶的所有檔案都會被視為敏感檔案,且自訂表情符號不能使用。被封鎖的伺服器不受影響。" muteAndBlock: "靜音和封鎖" mutedUsers: "被靜音的使用者" blockedUsers: "被封鎖的使用者" @@ -253,10 +239,10 @@ noCustomEmojis: "沒有自訂的表情符號" noJobs: "沒有任務" federating: "聯邦運作中" blocked: "已封鎖" -suspended: "停止發送" +suspended: "已凍結" all: "全部" subscribing: "訂閱中" -publishing: "發送中" +publishing: "直播中" notResponding: "沒有回應" instanceFollowing: "追隨的伺服器" instanceFollowers: "伺服器的追隨者" @@ -326,7 +312,6 @@ selectFile: "選擇檔案" selectFiles: "選擇檔案" selectFolder: "選擇資料夾" selectFolders: "選擇資料夾" -fileNotSelected: "尚未選擇檔案" renameFile: "重新命名檔案" folderName: "資料夾名稱" createFolder: "新增資料夾" @@ -353,7 +338,7 @@ reload: "重新整理" doNothing: "無視" reloadConfirm: "確定要重新整理嗎?" watch: "關注" -unwatch: "取消關注" +unwatch: "取消追隨" accept: "接受" reject: "拒絕" normal: "正常" @@ -380,7 +365,7 @@ enableRegistration: "開放新使用者註冊" invite: "邀請" driveCapacityPerLocalAccount: "每個本地使用者的雲端硬碟容量" driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小" -inMb: "以 MB 為單位" +inMb: "以Mbps為單位" bannerUrl: "橫幅圖片URL" backgroundImageUrl: "背景圖片的來源網址 " basicInfo: "基本資訊" @@ -392,21 +377,16 @@ pinnedClipId: "置頂的摘錄ID" pinnedNotes: "已置頂的貼文" hcaptcha: "hCaptcha" enableHcaptcha: "啟用 hCaptcha" -hcaptchaSiteKey: "hcaptchaSiteKey" -hcaptchaSecretKey: "hcaptchaSecretKey" -mcaptcha: "mCaptcha" -enableMcaptcha: "啟用 mCaptcha" -mcaptchaSiteKey: "網站金鑰" -mcaptchaSecretKey: "私密金鑰" -mcaptchaInstanceUrl: "mCaptcha 的實例網址" +hcaptchaSiteKey: "網站金鑰" +hcaptchaSecretKey: "金鑰" recaptcha: "reCAPTCHA" enableRecaptcha: "啟用 reCAPTCHA" recaptchaSiteKey: "網站金鑰" recaptchaSecretKey: "金鑰" turnstile: "Turnstile" enableTurnstile: "啟用 Turnstile" -turnstileSiteKey: "turnstileSiteKey" -turnstileSecretKey: "turnstileSecretKey" +turnstileSiteKey: "網站金鑰" +turnstileSecretKey: "金鑰" avoidMultiCaptchaConfirm: "使用多種驗證方式可能會造成干擾,您要關閉其他驗證方式嗎?您可以按「取消」保留多種驗證方式。" antennas: "天線" manageAntennas: "管理天線" @@ -414,7 +394,6 @@ name: "名稱" antennaSource: "接收來源" antennaKeywords: "包含關鍵字" antennaExcludeKeywords: "排除關鍵字" -antennaExcludeBots: "排除機器人帳戶" antennaKeywordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)" notifyAntenna: "通知有新貼文" withFileAntenna: "僅帶有附件的貼文" @@ -486,17 +465,15 @@ title: "標題" text: "文字" enable: "啟用" next: "下一步" -retype: "重新輸入" +retype: "再次輸入" noteOf: "{user}的貼文" inviteToGroup: "邀請至群組" quoteAttached: "引用" quoteQuestion: "是否要引用?" -attachAsFileQuestion: "剪貼簿的文字較長。請問是否要將其以文字檔的方式附加呢?" noMessagesYet: "沒有訊息" newMessageExists: "有新的訊息" onlyOneFileCanBeAttached: "只能加入一個附件" signinRequired: "請先登入" -signinOrContinueOnRemote: "若要繼續,需前往您所在的伺服器,或者註冊並登入此伺服器" invitations: "邀請" invitationCode: "邀請碼" checking: "確認中" @@ -523,7 +500,6 @@ disableDrawer: "不顯示下拉式選單" youHaveNoGroups: "找不到群組" joinOrCreateGroup: "請加入現有群組,或創建新群組。" showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項" -showReactionsCount: "顯示貼文的反應數目" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" enableAdvancedMfm: "啟用進階 MFM" @@ -628,13 +604,13 @@ menu: "選單" divider: "分隔線" addItem: "新增項目" rearrange: "排序方式" -relays: "中繼器" -addRelay: "新增中繼器" -inboxUrl: "收件夾 URL" -addedRelays: "已加入的中繼器" +relays: "中繼" +addRelay: "新增中繼" +inboxUrl: "收件夾URL" +addedRelays: "已加入的中繼" serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。" deletedNote: "已刪除的貼文" -invisibleNote: "私人貼文" +invisibleNote: "私密的貼文" enableInfiniteScroll: "啟用自動滾動頁面模式" visibility: "可見性" poll: "票選活動" @@ -662,7 +638,6 @@ medium: "中" small: "小" generateAccessToken: "發行存取權杖" permission: "權限" -adminPermission: "管理員權限" enableAll: "啟用全部" disableAll: "停用全部" tokenRequested: "允許存取帳戶" @@ -706,7 +681,6 @@ useGlobalSettingDesc: "啟用時,將使用帳戶通知設定。停用時,則 other: "其他" regenerateLoginToken: "重新產生登入權杖" regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。一般情況下是不需要這樣做的。重新產生後,所有裝置將會被登出。" -theKeywordWhenSearchingForCustomEmoji: "這是搜尋自訂表情符號時的關鍵字" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多個項目。" fileIdOrUrl: "檔案 ID 或 URL" behavior: "行為" @@ -720,7 +694,7 @@ abuseReported: "檢舉完成。感謝您的報告。" reporter: "檢舉者" reporteeOrigin: "檢舉來源" reporterOrigin: "檢舉者來源" -forwardReport: "將報告轉送給遠端伺服器" +forwardReport: "將報告轉送給遠端實例" forwardReportIsAnonymous: "在遠端實例上看不到您的資訊,顯示的報告者是匿名的系统帳戶。" send: "發送" abuseMarkAsResolved: "處理完畢" @@ -728,7 +702,7 @@ openInNewTab: "在新分頁中開啟" openInSideView: "在側欄中開啟" defaultNavigationBehaviour: "預設導航" editTheseSettingsMayBreakAccount: "修改這些設定可能會毀損您的帳戶" -instanceTicker: "貼文的伺服器資訊" +instanceTicker: "貼文的實例來源" waitingFor: "等待{x}" random: "隨機" system: "系統" @@ -780,7 +754,7 @@ experimentalFeatures: "實驗中的功能" experimental: "實驗性" thisIsExperimentalFeature: "這是實驗性的功能。可能會有變更規格和不能正常動作的可能性。" developer: "開發者" -makeExplorable: "使自己的帳戶更容易被找到" +makeExplorable: "使自己的帳戶能夠在「探索」頁面中顯示" makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。" showGapBetweenNotesInTimeline: "分開顯示時間軸上的貼文" duplicate: "複製" @@ -819,7 +793,7 @@ newVersionOfClientAvailable: "新版本的客戶端可用。" usageAmount: "使用量" capacity: "容量" inUse: "已使用" -editCode: "編輯程式碼" +editCode: "編輯代碼" apply: "套用" receiveAnnouncementFromInstance: "接收來自伺服器的通知" emailNotification: "郵件通知" @@ -849,7 +823,7 @@ active: "最近活躍" offline: "離線" notRecommended: "不推薦" botProtection: "Bot 防護" -instanceBlocking: "已封鎖或禁言的伺服器" +instanceBlocking: "已封鎖的實例" selectAccount: "選擇帳戶" switchAccount: "切換帳戶" enabled: "已啟用" @@ -860,7 +834,6 @@ administration: "管理" accounts: "帳戶" switch: "切換" noMaintainerInformationWarning: "尚未設定管理員訊息。" -noInquiryUrlWarning: "尚未設定聯絡表單網址。" noBotProtectionWarning: "尚未設定 Bot 防護。" configure: "設定" postToGallery: "發佈到相簿" @@ -995,7 +968,7 @@ cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此 cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。" beta: "測試版" enableAutoSensitive: "自動 NSFW 判定" -enableAutoSensitiveDescription: "如果可行,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依伺服器規則而自動啟用。" +enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依實例規則而自動啟用。" activeEmailValidationDescription: "主動地驗證使用者的電子郵件地址,以確定是否是一次性地址以及是否可以真正與其進行通訊。關閉時,僅檢查格式是否正確。" navbar: "導覽列" shuffle: "隨機" @@ -1005,7 +978,7 @@ pushNotification: "推播通知" subscribePushNotification: "啟用推播通知" unsubscribePushNotification: "停用推播通知" pushNotificationAlreadySubscribed: "推播通知啟用中" -pushNotificationNotSupported: "瀏覽器或伺服器不支援推播通知" +pushNotificationNotSupported: "瀏覽器或實例不支援推播通知" sendPushNotificationReadMessage: "如果已閱讀通知與訊息,就刪除推播通知" sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通知將立刻顯示。可能會更消耗裝置電池。" windowMaximize: "最大化" @@ -1024,7 +997,6 @@ neverShow: "不再顯示" remindMeLater: "以後再說" didYouLikeMisskey: "您喜歡 CherryPick 嗎?" pleaseDonate: "CherryPick 是由 {host} 使用的免費軟體。請贊助我們,讓開發得以持續!" -correspondingSourceIsAvailable: "對應的原始碼可以在 {anchor} 處找到。" roles: "角色" role: "角色" noRole: "沒有角色" @@ -1052,7 +1024,6 @@ thisPostMayBeAnnoyingHome: "發佈到首頁" thisPostMayBeAnnoyingCancel: "退出" thisPostMayBeAnnoyingIgnore: "直接發佈貼文" collapseRenotes: "省略顯示已看過的轉發貼文" -collapseRenotesDescription: "將已做過反應和轉發的貼文折疊顯示。" internalServerError: "內部伺服器錯誤" internalServerErrorDescription: "內部伺服器出現意外錯誤。" copyErrorInfo: "複製錯誤資訊" @@ -1076,9 +1047,6 @@ resetPasswordConfirm: "重設密碼?" sensitiveWords: "敏感詞" sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。" sensitiveWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。" -prohibitedWords: "禁語" -prohibitedWordsDescription: "當要發布包含禁語的貼文時,會出現錯誤。可以用換行分隔來設定多個禁語。" -prohibitedWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。" hiddenTags: "隱藏標籤" hiddenTagsDescription: "設定的標籤不會在趨勢中顯示,換行可以設定多個標籤。" notesSearchNotAvailable: "無法使用搜尋貼文功能。" @@ -1094,11 +1062,9 @@ enableChartsForFederatedInstances: "生成遠端伺服器的圖表" showClipButtonInNoteFooter: "新增摘錄按鈕至貼文" reactionsDisplaySize: "反應的顯示尺寸" limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。" -noteIdOrUrl: "貼文 ID 或 URL" +noteIdOrUrl: "貼文ID或URL" video: "影片" videos: "影片" -audio: "音效" -audioFiles: "音效檔案" dataSaver: "數據節省模式" accountMigration: "遷移帳戶" accountMoved: "這個使用者已遷移至新的帳戶:" @@ -1109,7 +1075,7 @@ addMemo: "新增備註" editMemo: "編輯備註" reactionsList: "反應列表" renotesList: "轉發貼文列表" -notificationDisplay: "通知" +notificationDisplay: "通知的顯示" leftTop: "左上" rightTop: "右上" leftBottom: "左下" @@ -1126,8 +1092,6 @@ preservedUsernames: "保留的使用者名稱" preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。" createNoteFromTheFile: "由此檔案建立貼文" archive: "封存" -archived: "已封存" -unarchive: "取消封存" channelArchiveConfirmTitle: "要封存{name}嗎?" channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。" thisChannelArchived: "這個頻道已被封存。" @@ -1138,9 +1102,6 @@ preventAiLearning: "拒絕接受生成式AI的訓練" preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤,而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。" options: "選項" specifyUser: "指定使用者" -lookupConfirm: "要查詢嗎?" -openTagPageConfirm: "要開啟標籤的頁面嗎?" -specifyHost: "指定主機" failedToPreviewUrl: "無法預覽" update: "更新" rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色" @@ -1199,7 +1160,6 @@ showRenotes: "顯示其他人的轉發貼文" edited: "已編輯" notificationRecieveConfig: "接受通知的設定" mutualFollow: "互相追隨" -followingOrFollower: "追隨中或追隨者" fileAttachedOnly: "顯示包含附件的貼文" showRepliesToOthersInTimeline: "顯示給其他人的回覆" hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆" @@ -1208,23 +1168,16 @@ hideRepliesToOthersInTimelineAll: "在時間軸不包含追隨中所有人的回 confirmShowRepliesAll: "進行此操作後無法復原。您真的希望時間軸「包含」您目前追隨的所有人的回覆嗎?" confirmHideRepliesAll: "進行此操作後無法復原。您真的希望時間軸「不包含」您目前追隨的所有人的回覆嗎?" externalServices: "外部服務" -sourceCode: "原始碼" -sourceCodeIsNotYetProvided: "尚未提供原始碼,請洽詢管理員解決這個問題。" -repositoryUrl: "儲存庫 URL" -repositoryUrlDescription: "如果存在可公開取得原始碼的儲存庫,請輸入其 URL。 如果您按原樣使用 CherryPick(不對原始碼進行任何更改),請輸入 https://github.com/kokonect-link/cherrypick。" -repositoryUrlOrTarballRequired: "如果儲存庫不是公開的,則必須提供 tarball。 詳細資訊請參閱 .config/example.yml。" -feedback: "意見回饋" -feedbackUrl: "意見回饋 URL" impressum: "營運者資訊" -impressumUrl: "營運者資訊 URL" +impressumUrl: "營運者資訊網址" impressumDescription: "在德國與部份地區必須要明確顯示營運者資訊。" privacyPolicy: "隱私政策" -privacyPolicyUrl: "隱私政策 URL" +privacyPolicyUrl: "隱私政策網址" tosAndPrivacyPolicy: "服務條款和隱私政策" avatarDecorations: "頭像裝飾" attach: "裝上" detach: "取下" -detachAll: "全部移除" +detachAll: "移除所有裝飾" angle: "角度" flip: "翻轉" showAvatarDecorations: "顯示頭像裝飾" @@ -1242,65 +1195,8 @@ remainingN: "剩餘:{n}" overwriteContentConfirm: "確定要覆蓋目前的內容嗎?" seasonalScreenEffect: "隨季節變換畫面的呈現" decorate: "設置頭像裝飾" -addMfmFunction: "插入 MFM 功能語法" -enableQuickAddMfmFunction: "顯示高級 MFM 選擇器" -bubbleGame: "氣泡遊戲" -sfx: "音效" -soundWillBePlayed: "將播放音效" -showReplay: "觀看重播" -replay: "重播" -replaying: "重播中" -endReplay: "退出重播" -copyReplayData: "複製重播資料" -ranking: "排行榜" -lastNDays: "過去 {n} 天" -backToTitle: "回到遊戲標題頁" -hemisphere: "您居住的地區" -withSensitive: "顯示包含敏感檔案的貼文" -userSaysSomethingSensitive: "包含 {name} 敏感檔案的貼文" -enableHorizontalSwipe: "滑動切換時間軸" -loading: "載入中" -surrender: "退出" -gameRetry: "再試一次" -notUsePleaseLeaveBlank: "如果不使用的話請留白" -useTotp: "使用一次性密碼" -useBackupCode: "使用備用驗證碼" -launchApp: "啟動 APP" -useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊" -keepOriginalFilename: "保留原始檔名" -keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。" -noDescription: "沒有說明文字" -alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息" -inquiry: "聯絡我們" -tryAgain: "請再試一次。" -confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認" -sensitiveMediaRevealConfirm: "這是敏感媒體。確定要顯示嗎?" -createdLists: "已建立的清單" -createdAntennas: "已建立的天線" -_delivery: - status: "傳送狀態" - stop: "停止發送" - resume: "恢復發送" - _type: - none: "發送中" - manuallySuspended: "手動暫停中" - goneSuspended: "因為伺服器刪除所以暫停中" - autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中" -_bubbleGame: - howToPlay: "玩法說明" - hold: "保留" - _score: - score: "分數" - scoreYen: "賺取的金額" - highScore: "最高分" - maxChain: "最大結合數" - yen: "{yen}円" - estimatedQty: "{qty}個" - scoreSweets: "飯糰 {onigiriQtyWithUnit}" - _howToPlay: - section1: "調整位置並將物體放入盒子中。" - section2: "當相同類型的物體黏在一起時,它們會變成不同的物體,您就會得到分數。" - section3: "如果物體從盒子裡溢出,遊戲就結束了。透過融合物體而不溢出盒子來獲得高分!" +addMfmFunction: "插入MFM功能語法" +enableQuickAddMfmFunction: "顯示高級MFM選擇器" _announcement: forExistingUsers: "僅限既有的使用者" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" @@ -1310,7 +1206,7 @@ _announcement: tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。" readConfirmTitle: "標記為已讀嗎?" readConfirmText: "閱讀「{title}」的內容並標記為已讀。" - shouldNotBeUsedToPresentPermanentInfo: "為了避免損害新用戶的使用體驗,建議使用公告來發布即時性的訊息,而不是用於固定不變的資訊。" + shouldNotBeUsedToPresentPermanentInfo: "由於可能會破壞使用者體驗,尤其是對於新使用者而言,我們建議使用公告來發布有時效性的資訊而不是固定不變的資訊。" dialogAnnouncementUxWarn: "如果同時有 2 個以上對話方塊形式的公告存在,對於使用者體驗很可能會有不良的影響,因此建議謹慎使用。" silence: "不發送通知" silenceDescription: "啟用此選項後,將不會發送此公告的通知,並且無需將其標記為已讀。" @@ -1322,7 +1218,7 @@ _initialAccountSetting: privacySetting: "隱私設定" theseSettingsCanEditLater: "這裡的設定可以在之後變更。" youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。" - followUsers: "為了構築時間軸,試著追隨您感興趣的使用者吧。" + followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。" pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。" initialAccountSettingCompleted: "初始設定完成了!" haveFun: "盡情享受{name}吧!" @@ -1377,7 +1273,7 @@ _initialTutorial: title: "隱藏內容(CW)" description: "將顯示「註釋」中寫入的內容而不是本文。按一下「顯示內容」以顯示本文。" _exampleNote: - cw: "注意消夜文" + cw: "美食恐怖主義注意" note: "我吃了一個巧克力甜甜圈🍩😋" useCases: "伺服器的服務條款可能會規範特定的貼文需要使用隱藏內容,除此之外也會用在隱藏劇情洩漏與敏感內容的貼文。" _howToMakeAttachmentsSensitive: @@ -1402,7 +1298,7 @@ _serverRules: _serverSettings: iconUrl: "圖示的 URL" appIconDescription: "指定顯示 {host} 為應用程式時的圖示。" - appIconUsageExample: "例如:PWA 或是在手機桌面作為書籤等" + appIconUsageExample: "例如:漸進式網路應用程式(PWA)、於手機桌面新增書籤" appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。" appIconResolutionMustBe: "解析度必須為 {resolution}。" manifestJsonOverride: "覆寫 manifest.json" @@ -1411,12 +1307,10 @@ _serverSettings: fanoutTimelineDescription: "如果啟用的話,檢索各個時間軸的性能會顯著提昇,資料庫的負荷也會減少。不過,Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。" fanoutTimelineDbFallback: "資料庫的回退" fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。" - inquiryUrl: "聯絡表單網址" - inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。" _accountMigration: moveFrom: "從其他帳戶遷移到這個帳戶" moveFromSub: "為另一個帳戶建立別名" - moveFromLabel: "要遷移過來的帳戶 #{n}" + moveFromLabel: "要遷移過來的帳戶:" moveFromDescription: "如果你想把追隨者從別的帳戶遷移過來,必須先在這裡建立別名。請務必在執行遷移之前建立別名!請像這樣輸入要遷移的帳戶:@person@instance.com" moveTo: "將這個帳戶遷移至新的帳戶" moveToLabel: "要遷移到的帳戶:" @@ -1484,7 +1378,7 @@ _achievements: _login3: title: "初學者Ⅰ" description: "總登入天數為三天" - flavor: "從今天開始,我就是 Cherrypikist" + flavor: "從今天開始,我就是 Misskist" _login7: title: "初學者ⅠⅠ" description: "總登入天數為七天" @@ -1589,7 +1483,7 @@ _achievements: _viewAchievements3min: title: "成就發燒友" description: "看著成就列表超過三分鐘" - _iLoveCherryPick: + _iLoveMisskey: title: "I Love CherryPick" description: "發佈「I ❤ #CherryPick」" flavor: "感謝您使用 CherryPick!by 開發團隊" @@ -1612,7 +1506,7 @@ _achievements: _postedAt0min0sec: title: "報時" description: "在零分零秒發佈貼文" - flavor: "啵.啵.啵.嗶ー" + flavor: "啵、啵、啵、嗶ーー" _selfQuote: title: "自我引用" description: "引用了自己的貼文" @@ -1621,7 +1515,7 @@ _achievements: description: "首頁時間軸在一分鐘內出現超過二十篇貼文" _viewInstanceChart: title: "分析師" - description: "顯示了伺服器的圖表" + description: "顯示了實例的圖表" _outputHelloWorldOnScratchpad: title: "Hello, world!" description: "在 AiScript 控制臺輸出了「hello world」" @@ -1676,20 +1570,13 @@ _achievements: _tutorialCompleted: title: "CherryPick新手講座 結業證書" description: "已完成教學課程" - _bubbleGameExplodingHead: - title: "🤯" - description: "氣泡遊戲中最大的物體出現了" - _bubbleGameDoubleExplodingHead: - title: "雙重🤯" - description: "氣泡遊戲中最大的物體同時出現了兩個" - flavor: "這樣大小的便當盒,用 🤯 🤯 稍微裝滿一些吧" _role: new: "建立角色" edit: "編輯角色" name: "角色名稱" description: "角色描述 " permission: "角色的權限" - descriptionOfPermission: "審查員執行與審查相關的基本操作。\n管理員能變更伺服器的全部設定。" + descriptionOfPermission: "審查員執行與審查相關的基本操作。\n管理員能變更實例的全部設定" assignTarget: "指派目標" descriptionOfAssignTarget: "手動是以手動管理這個角色包含的人員。\n符合條件是設定條件以自動包含符合條件的使用者。" manual: "手動" @@ -1705,7 +1592,7 @@ _role: baseRole: "基本角色" useBaseValue: "使用基本角色的值" chooseRoleToAssign: "選擇要指派的角色" - iconUrl: "圖示的 URL" + iconUrl: "圖示的URL" asBadge: "顯示為徽章" descriptionOfAsBadge: "開啟的話,角色圖示會顯示在使用者名稱旁邊。" isExplorable: "讓使用者更容易找到您" @@ -1724,8 +1611,7 @@ _role: ltlAvailable: "瀏覽本地時間軸" canPublicNote: "允許公開貼文" canEditNote: "允許編輯貼文" - mentionMax: "貼文內的最大提及數" - canInvite: "發行伺服器邀請碼" + canInvite: "發行實例邀請碼" inviteLimit: "可建立邀請碼的數量" inviteLimitCycle: "邀請碼的發放間隔" inviteExpirationTime: "邀請碼的有效日期" @@ -1733,7 +1619,6 @@ _role: canManageAvatarDecorations: "管理頭像裝飾" driveCapacity: "雲端硬碟容量" alwaysMarkNsfw: "總是將檔案標記為NSFW" - canUpdateBioMedia: "允許更新大頭貼和橫幅" pinMax: "置頂貼文的最大數量" antennaMax: "可建立的天線數量" wordMuteMax: "靜音文字的最大字數" @@ -1749,14 +1634,8 @@ _role: canUseTranslator: "使用翻譯功能" avatarDecorationLimit: "頭像裝飾的最大設置量" _condition: - roleAssignedTo: "手動指派角色完成" isLocal: "本地使用者" isRemote: "遠端使用者" - isCat: "貓使用者" - isBot: "機器人使用者" - isSuspended: "被停權的使用者" - isLocked: "上鎖的使用者" - isExplorable: "開啟了「使您的帳戶更容易被找到」功能的使用者" createdLessThan: "帳戶加入時間不超過" createdMoreThan: "帳戶加入時間已超過" followersLessThanOrEq: "追隨者人數在~以下" @@ -1810,7 +1689,7 @@ _ad: _forgotPassword: enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。" ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 " - contactAdmin: "本伺服器不支援電子郵件,請聯繫您的管理員重置您的密碼。 " + contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。 " _gallery: my: "我的貼文" liked: "喜歡的貼文" @@ -1826,7 +1705,6 @@ _plugin: installWarn: "請不要安裝來源不明的外掛。" manage: "管理外掛" viewSource: "檢視原始碼" - viewLog: "顯示記錄 " _preferencesBackups: list: "已備份的設定檔" saveNew: "另存新檔" @@ -1856,8 +1734,6 @@ _aboutMisskey: contributors: "主要貢獻者" allContributors: "全體貢獻人員" source: "原始碼" - original: "原始" - thisIsModifiedVersion: "{name} 使用原始 CherryPick 的修改版本。" translation: "翻譯 Misskey" donate: "贊助 Misskey" morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" @@ -1966,8 +1842,8 @@ _wordMute: _instanceMute: instanceMuteDescription: "包括對被靜音伺服器上的使用者的回覆,被設定的伺服器上所有貼文及轉發都會被靜音。" instanceMuteDescription2: "設定時以換行進行分隔" - title: "將隱藏被設定的伺服器貼文。" - heading: "將伺服器靜音" + title: "將隱藏被設定的實例貼文。" + heading: "將實例靜音" _theme: explore: "探索佈景主題" install: "安裝佈景主題" @@ -1981,7 +1857,7 @@ _theme: invalid: "佈景主題格式錯誤" make: "製作佈景主題" base: "基於" - addConstant: "新增常數" + addConstant: "添加常數" constant: "常數" defaultValue: "預設值" color: "顏色" @@ -2045,6 +1921,8 @@ _sfx: notification: "通知" chat: "聊天" chatBg: "聊天背景" + antenna: "天線接收" + channel: "頻道通知" reaction: "選擇反應時" _soundSettings: driveFile: "使用雲端硬碟的音效檔案" @@ -2053,26 +1931,25 @@ _soundSettings: driveFileTypeWarnDescription: "請選擇音效檔案" driveFileDurationWarn: "音效太長了" driveFileDurationWarnDescription: "使用長音效檔可能會影響 CherryPick 的使用體驗。仍要使用此檔案嗎?" - driveFileError: "無法載入語音。請更改設定" _ago: future: "未來" justNow: "剛剛" - secondsAgo: "{n}秒前" - minutesAgo: "{n}分鐘前" - hoursAgo: "{n}小時前" - daysAgo: "{n}天前" - weeksAgo: "{n}周前" - monthsAgo: "{n}個月前" - yearsAgo: "{n}年前" + secondsAgo: "{n} 秒前" + minutesAgo: "{n} 分鐘前 " + hoursAgo: "{n} 小時前" + daysAgo: "{n} 天前" + weeksAgo: "{n} 週前" + monthsAgo: "{n} 個月前" + yearsAgo: "{n} 年前" invalid: "無" _timeIn: - seconds: "{n}秒後" - minutes: "{n}分鐘後" - hours: "{n}小時後" - days: "{n}天後" - weeks: "{n}周後" - months: "{n}個月後" - years: "{n}年後" + seconds: "{n} 秒後" + minutes: "{n} 分後" + hours: "{n} 小時後" + days: "{n} 日後" + weeks: "{n} 週後" + months: "{n} 個月後" + years: "{n} 年後" _time: second: "秒" minute: "分鐘" @@ -2083,6 +1960,7 @@ _2fa: registerTOTP: "開始設定驗證應用程式" step1: "首先,在您的裝置上安裝驗證程式,例如 {a} 或 {b}。" step2: "然後,掃描螢幕上的 QR 碼。" + step2Click: "您可以點擊 QR 碼,以使用裝置上的驗證應用程式或金鑰環註冊。" step2Uri: "使用桌面版應用程式時,請輸入以下的 URI" step3Title: "輸入驗證碼" step3: "輸入應用程式所提供的權杖以完成設定。" @@ -2106,7 +1984,6 @@ _2fa: backupCodesDescription: "如果驗證應用程式不能用了,可以使用以下的備用驗證碼存取您的帳戶。請務必妥善保管這個驗證碼。每個驗證碼只能使用一次。" backupCodeUsedWarning: "已使用備用驗證碼。如果無法使用驗證應用程式,請盡快重新設定。" backupCodesExhaustedWarning: "已使用所有備用驗證碼。如果無法使用驗證應用程式,則將無法再存取您的帳戶。請重新設定您的驗證應用程式。" - moreDetailedGuideHere: "請點擊此處查看詳細說明。" _permissions: "read:account": "查看我的帳戶資訊" "write:account": "更改我的帳戶資訊" @@ -2136,62 +2013,14 @@ _permissions: "write:user-groups": "編輯使用者群組" "read:channels": "已查看的頻道" "write:channels": "編輯頻道" - "read:gallery": "瀏覽相簿" - "write:gallery": "編輯相簿" - "read:gallery-likes": "瀏覽相簿的讚" - "write:gallery-likes": "編輯相簿的讚" + "read:gallery": "瀏覽圖庫" + "write:gallery": "操作圖庫" + "read:gallery-likes": "讀取喜歡的圖片" + "write:gallery-likes": "操作喜歡的圖片" "read:flash": "檢視 Play" "write:flash": "編輯 Play" "read:flash-likes": "檢視 Play 的讚" "write:flash-likes": "編輯 Play 的讚" - "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": "查看使用者的 IP 位址" - "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: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": "查看使用者雲端硬碟的相關資訊" - "read:admin:stream": "使用管理員的 Websocket API" - "write:admin:ad": "編輯廣告" - "read:admin:ad": "查看廣告" - "write:invite-codes": "建立邀請碼" - "read:invite-codes": "取得邀請碼" - "write:clip-favorite": "編輯摘錄的讚" - "read:clip-favorite": "查看摘錄的讚" - "read:federation": "查看站台聯邦的相關資訊" - "write:report-abuse": "檢舉違規行為" _auth: shareAccessTitle: "應用程式的存取權限" shareAccess: "要授權「“{name}”」存取您的帳戶嗎?" @@ -2210,16 +2039,16 @@ _antennaSources: userGroup: "來自特定群組的貼文" userBlacklist: "除指定使用者外的所有貼文" _weekday: - sunday: "星期天" - monday: "星期一" - tuesday: "星期二" - wednesday: "星期三" - thursday: "星期四" - friday: "星期五" - saturday: "星期六" + sunday: "週日" + monday: "週一" + tuesday: "週二" + wednesday: "週三" + thursday: "週四" + friday: "週五" + saturday: "週六" _widgets: profile: "個人檔案" - instanceInfo: "伺服器資訊" + instanceInfo: "實例資訊" memo: "備忘錄" notifications: "通知" timeline: "時間軸" @@ -2233,7 +2062,7 @@ _widgets: digitalClock: "電子時鐘" unixClock: "UNIX 時間" federation: "聯邦宇宙" - instanceCloud: "伺服器雲" + instanceCloud: "實例雲" postForm: "發文視窗" slideshow: "幻燈片" button: "按鈕" @@ -2265,8 +2094,8 @@ _poll: deadlineDate: "截止日期" deadlineTime: "小時" duration: "時長" - votesCount: "{n}票" - totalVotes: "合計 {n} 票" + votesCount: "{n} 票" + totalVotes: "一共{n}票" vote: "投票" showResult: "顯示結果" voted: "已投票" @@ -2285,7 +2114,7 @@ _visibility: specified: "指定使用者" specifiedDescription: "僅發布至指定使用者" disableFederation: "停用聯邦" - disableFederationDescription: "不發送到其他伺服器" + disableFederationDescription: "不要傳遞給其他實例" _postForm: replyPlaceholder: "回覆此貼文..." quotePlaceholder: "引用此貼文..." @@ -2298,7 +2127,7 @@ _postForm: e: "寫些什麼吧……" f: "靜待發文……" _profile: - name: "名字" + name: "名稱" username: "使用者名稱" description: "關於我" youCanIncludeHashtags: "你也可以在「關於我」中加上 #tag" @@ -2314,7 +2143,6 @@ _profile: _exportOrImport: allNotes: "所有貼文" favoritedNotes: "「我的最愛」貼文" - clips: "摘錄" followingList: "追隨中" muteList: "靜音" blockingList: "封鎖" @@ -2326,12 +2154,12 @@ _charts: federation: "聯邦宇宙" apRequest: "請求" usersIncDec: "使用者增減" - usersTotal: "使用者合計" + usersTotal: "使用者總數" activeUsers: "活躍使用者" notesIncDec: "貼文増減" localNotesIncDec: "本地貼文増減" remoteNotesIncDec: "遠端貼文數目增减" - notesTotal: "貼文總數" + notesTotal: "貼文合共" filesIncDec: "檔案增減" filesTotal: "檔案總數" storageUsageIncDec: "儲存空間增減" @@ -2357,10 +2185,10 @@ _timelines: _play: new: "新增 Play" edit: "編輯 Play" - created: "已新增 Play " - updated: "已更新 Play " + created: "已新增Play " + updated: "已更新Play " deleted: "已刪除 Play" - pageSetting: "Play 設定" + pageSetting: "Play設定" editThisPage: "編輯此 Play" viewSource: "檢視原始碼" my: "自己的 Play" @@ -2369,11 +2197,10 @@ _play: title: "標題" script: "腳本" summary: "描述" - visibilityDescription: "如果您將其設為私密,它將不再顯示在您的個人資料中,但知道該 URL 的人仍然可以存取它。" _pages: newPage: "建立頁面" editPage: "編輯頁面" - readPage: "正在檢視原始碼" + readPage: "正檢視原始碼" created: "頁面已建立" updated: "頁面已更新" deleted: "頁面已被刪除" @@ -2400,7 +2227,7 @@ _pages: hideTitleWhenPinned: "被置頂於個人資料時隱藏頁面標題" font: "字型" fontSerif: "襯線體" - fontSansSerif: "黑體" + fontSansSerif: "無襯線體" eyeCatchingImageSet: "設定封面影像" eyeCatchingImageRemove: "刪除封面影像" chooseBlock: "新增方塊" @@ -2414,8 +2241,6 @@ _pages: section: "區段" image: "圖片" button: "按鈕" - dynamic: "動態方塊" - dynamicDescription: "這個方塊已經廢止,現在開始請使用 {play}。" note: "嵌式貼文" _note: id: "貼文ID" @@ -2446,10 +2271,8 @@ _notification: sendTestNotification: "發送測試通知" notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示" reactedBySomeUsers: "{n}人做出了反應" - likedBySomeUsers: "{n} 人按了讚" renotedBySomeUsers: "{n}人做了轉發" followedBySomeUsers: "被{n}人追隨了" - flushNotification: "重置通知歷史紀錄" _types: all: "全部 " note: "使用者的最新貼文" @@ -2474,7 +2297,6 @@ _deck: alwaysShowMainColumn: "總是顯示主欄" columnAlign: "對齊欄位" addColumn: "新增欄位" - newNoteNotificationSettings: "新貼文通知的設定" configureColumn: "欄位的設定" swapLeft: "向左移動" swapRight: "向右移動" @@ -2513,10 +2335,9 @@ _drivecleaner: orderByCreatedAtAsc: "按新增日期降序排列" _webhookSettings: createWebhook: "建立 Webhook" - modifyWebhook: "編輯 Webhook" - name: "名字" + name: "名稱" secret: "密鑰" - trigger: "觸發器" + events: "何時運行 Webhook" active: "已啟用" _events: follow: "當你追隨時" @@ -2526,25 +2347,6 @@ _webhookSettings: renote: "當被轉發時" reaction: "當獲得反應時" mention: "當被提到時" - _systemEvents: - abuseReport: "當使用者檢舉時" - abuseReportResolved: "當處理了使用者的檢舉時" - deleteConfirm: "請問是否要刪除 Webhook?" -_abuseReport: - _notificationRecipient: - createRecipient: "新增接收檢舉的通知對象" - modifyRecipient: "編輯接收檢舉的通知對象" - recipientType: "通知對象的種類" - _recipientType: - mail: "電子郵件" - webhook: "Webhook" - _captions: - mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)" - webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)" - keywords: "關鍵字" - notifiedUser: "被通知的使用者" - notifiedWebhook: "使用的 Webhook" - deleteConfirm: "確定要刪除通知對象嗎?" _moderationLogTypes: createRole: "新增角色" deleteRole: "刪除角色 " @@ -2557,7 +2359,7 @@ _moderationLogTypes: updateCustomEmoji: "更新自訂表情符號" deleteCustomEmoji: "刪除自訂表情符號" updateServerSettings: "更新伺服器設定" - updateUserNote: "更新了使用者的管理筆記" + updateUserNote: "更新管理筆記" deleteDriveFile: "刪除檔案" deleteNote: "刪除貼文" createGlobalAnnouncement: "建立全網通知" @@ -2569,7 +2371,6 @@ _moderationLogTypes: resetPassword: "重設密碼" suspendRemoteInstance: "封鎖遠端伺服器" unsuspendRemoteInstance: "解除封鎖遠端伺服器" - updateRemoteInstanceNote: "更新了遠端伺服器的管理筆記" markSensitiveDriveFile: "標記為敏感檔案" unmarkSensitiveDriveFile: "撤銷標記為敏感檔案" resolveAbuseReport: "解決檢舉" @@ -2582,12 +2383,6 @@ _moderationLogTypes: deleteAvatarDecoration: "刪除頭像裝飾" unsetUserAvatar: "移除使用者的大頭貼" unsetUserBanner: "移除使用者的橫幅圖像" - createSystemWebhook: "建立 SystemWebhook" - updateSystemWebhook: "更新 SystemWebhook" - deleteSystemWebhook: "刪除 SystemWebhook" - createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象" - updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象" - deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象" _fileViewer: title: "檔案詳細資訊" type: "檔案類型 " @@ -2650,76 +2445,3 @@ _dataSaver: _code: title: "程式碼突出顯示" description: "如果使用了 MFM 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。" -_hemisphere: - N: "北半球" - S: "南半球" - caption: "在某些客戶端的設定中,用於判斷季節。" -_reversi: - reversi: "黑白棋" - gameSettings: "對弈設定" - chooseBoard: "選擇棋盤" - blackOrWhite: "先手/後手" - blackIs: "{name} 為黑棋(先攻)" - rules: "規則" - thisGameIsStartedSoon: "對弈即將開始" - waitingForOther: "等待對手準備就緒" - waitingForMe: "等待您準備就緒" - waitingBoth: "請準備" - ready: "準備就緒" - cancelReady: "重新準備" - opponentTurn: "對手的回合" - myTurn: "您的回合" - turnOf: "{name} 的回合" - pastTurnOf: "{name} 的回合" - surrender: "認輸" - surrendered: "對手認輸" - timeout: "時間到" - drawn: "平手" - won: "{name} 獲勝" - black: "黑" - white: "白" - total: "合計" - turnCount: "{count} 回合" - myGames: "我的對弈" - allGames: "所有對弈" - ended: "已結束" - playing: "正在對弈" - isLlotheo: "子較少的一方為勝(顛倒規則)" - loopedMap: "循環棋盤" - canPutEverywhere: "隨意置放模式" - timeLimitForEachTurn: "每回合的時間限制" - freeMatch: "自由對戰" - lookingForPlayer: "正在搜尋對手" - gameCanceled: "對弈已被取消" - shareToTlTheGameWhenStart: "在遊戲開始時將對弈資訊發布到時間軸" - iStartedAGame: "對弈開始了! #MisskeyReversi" - opponentHasSettingsChanged: "對手更改了設定" - allowIrregularRules: "允許異常規則(完全自由)" - disallowIrregularRules: "不允許異常規則" - showBoardLabels: "在棋盤上顯示行、列號" - useAvatarAsStone: "用大頭貼當作棋子" -_offlineScreen: - title: "離線-無法連接伺服器" - header: "無法連接伺服器" -_urlPreviewSetting: - title: "URL 預覽設定" - enable: "啟用 URL 預覽" - timeout: "取得預覽的逾時時間 (ms)" - timeoutDescription: "若取得預覽所需的時間超過這個值,則不會產生預覽。" - maximumContentLength: "Content-Length 的最大値 (byte)" - maximumContentLengthDescription: "若 Content-Length 超過這個值,則不會產生預覽。" - requireContentLength: "僅在能夠取得 Content-Length 時,才產生預覽。" - requireContentLengthDescription: "若對方的伺服器未回傳 Content -Length,則不會產生預覽。" - userAgent: "User-Agent" - userAgentDescription: "設定獲取預覽時使用的 User-Agent 。如果留空,將使用預設的 User-Agent 。" - summaryProxy: "產生預覽的代理端點" - summaryProxyDescription: "使用摘要代理程式而不是 Misskey 本身產生預覽。" - summaryProxyDescription2: "以下參數會作為查詢字串連結到代理。如果代理端不支援,這些設定將被忽略。" -_mediaControls: - pip: "畫中畫" - playbackRate: "播放速度" - loop: "循環播放" -_contextMenu: - title: "內容功能表" - app: "應用程式" - native: "瀏覽器的使用者介面" diff --git a/misskey-assets b/misskey-assets new file mode 160000 index 0000000000..cf3ce27b2e --- /dev/null +++ b/misskey-assets @@ -0,0 +1 @@ +Subproject commit cf3ce27b2eb8417233072e3d6d2fb7c5356c2364 diff --git a/package.json b/package.json index e7a3da55b0..fd6c117184 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,17 @@ { "name": "cherrypick", - "version": "4.10.0-rc.3-engawa0.5.0", - "basedMisskeyVersion": "2024.7.0", + "version": "4.6.0", + "basedMisskeyVersion": "2023.12.2", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/kokonect-link/cherrypick.git" }, - "packageManager": "pnpm@9.6.0", + "packageManager": "pnpm@8.12.1", "workspaces": [ "packages/frontend", "packages/backend", - "packages/sw", - "packages/cherrypick-js", - "packages/misskey-bubble-game" + "packages/sw" ], "private": true, "scripts": { @@ -21,7 +19,7 @@ "build-assets": "node ./scripts/build-assets.mjs", "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", - "build-cherrypick-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!cherrypick-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/cherrypick-js/generator/api.json && pnpm --filter cherrypick-js update-autogen-code && pnpm --filter cherrypick-js build && pnpm --filter cherrypick-js api", + "build-cherrypick-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/cherrypick-js/generator/api.json && pnpm --filter cherrypick-js update-autogen-code && pnpm --filter cherrypick-js build", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", "start:docker": "pnpm check:connect && cd packages/backend && exec node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", @@ -51,29 +49,21 @@ "lodash": "4.17.21" }, "dependencies": { - "cssnano": "6.1.2", "execa": "8.0.1", - "fast-glob": "3.3.2", - "ignore-walk": "6.0.5", + "cssnano": "6.0.2", "js-yaml": "4.1.0", - "postcss": "8.4.40", - "tar": "6.2.1", - "terser": "5.31.3", - "typescript": "5.5.4", - "esbuild": "0.23.0", - "glob": "11.0.0" + "postcss": "8.4.32", + "terser": "5.26.0", + "typescript": "5.3.3" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "2.0.2", - "@types/node": "20.14.12", - "@typescript-eslint/eslint-plugin": "7.17.0", - "@typescript-eslint/parser": "7.17.0", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", "cross-env": "7.0.3", - "cypress": "13.13.1", - "eslint": "9.8.0", - "globals": "15.8.0", - "ncp": "2.0.0", - "start-server-and-test": "2.0.4" + "cypress": "13.6.1", + "eslint": "8.56.0", + "start-server-and-test": "2.0.3", + "ncp": "2.0.0" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.4.0" diff --git a/packages/backend/.eslintignore b/packages/backend/.eslintignore new file mode 100644 index 0000000000..790eb90145 --- /dev/null +++ b/packages/backend/.eslintignore @@ -0,0 +1,4 @@ +node_modules +/built +/.eslintrc.js +/@types/**/* diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs new file mode 100644 index 0000000000..f9fe4814e6 --- /dev/null +++ b/packages/backend/.eslintrc.cjs @@ -0,0 +1,32 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json', './test/tsconfig.json'], + }, + extends: [ + '../shared/.eslintrc.js', + ], + rules: { + 'import/order': ['warn', { + 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + 'pathGroups': [ + { + 'pattern': '@/**', + 'group': 'external', + 'position': 'after' + } + ], + }], + 'no-restricted-globals': [ + 'error', + { + 'name': '__dirname', + 'message': 'Not in ESModule. Use `import.meta.url` instead.' + }, + { + 'name': '__filename', + 'message': 'Not in ESModule. Use `import.meta.url` instead.' + } + ] + }, +}; diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc index 845190b5f4..0504a2d389 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -19,6 +19,5 @@ }, "target": "es2022" }, - "minify": false, - "sourceMaps": "inline" + "minify": false } diff --git a/packages/backend/assets/api-doc.html b/packages/backend/assets/api-doc.html deleted file mode 100644 index 2c0b8e640c..0000000000 --- a/packages/backend/assets/api-doc.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - CherryPick API - - - - - - - - - diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html new file mode 100644 index 0000000000..e41d47f046 --- /dev/null +++ b/packages/backend/assets/redoc.html @@ -0,0 +1,24 @@ + + + + CherryPick API + + + + + + + + + + + + + diff --git a/packages/backend/biome.json b/packages/backend/biome.json deleted file mode 100644 index 2e2c0a7871..0000000000 --- a/packages/backend/biome.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", - "organizeImports": { "enabled": true }, - "linter": { - "enabled": true, - "rules": { - "recommended": false, - "complexity": { - "noBannedTypes": "error", - "noExtraBooleanCast": "error", - "noMultipleSpacesInRegularExpressionLiterals": "error", - "noUselessCatch": "error", - "noUselessTypeConstraint": "error", - "noWith": "error" - }, - "correctness": { - "noConstAssign": "error", - "noConstantCondition": "warn", - "noEmptyCharacterClassInRegex": "error", - "noEmptyPattern": "warn", - "noGlobalObjectCalls": "error", - "noInnerDeclarations": "off", - "noInvalidConstructorSuper": "error", - "noNewSymbol": "error", - "noNonoctalDecimalEscape": "error", - "noPrecisionLoss": "error", - "noSelfAssign": "error", - "noSetterReturn": "error", - "noSwitchDeclarations": "error", - "noUndeclaredVariables": "error", - "noUnreachable": "error", - "noUnreachableSuper": "error", - "noUnsafeFinally": "error", - "noUnsafeOptionalChaining": "error", - "noUnusedLabels": "error", - "noUnusedVariables": "error", - "useArrayLiterals": "off", - "useIsNan": "error", - "useValidForDirection": "error", - "useYield": "error" - }, - "style": { - "noDefaultExport": "warn", - "noInferrableTypes": "warn", - "noNamespace": "error", - "noNonNullAssertion": "warn", - "noParameterAssign": "warn", - "noRestrictedGlobals": { - "level": "error", - "options": { "deniedGlobals": ["__dirname", "__filename"] } - }, - "noVar": "error", - "useAsConstAssertion": "error" - }, - "suspicious": { - "noAsyncPromiseExecutor": "off", - "noCatchAssign": "error", - "noClassAssign": "error", - "noCompareNegZero": "error", - "noControlCharactersInRegex": "warn", - "noDebugger": "error", - "noDoubleEquals": "error", - "noDuplicateCase": "error", - "noDuplicateClassMembers": "error", - "noDuplicateObjectKeys": "error", - "noDuplicateParameters": "error", - "noEmptyBlockStatements": "off", - "noExplicitAny": "warn", - "noExtraNonNullAssertion": "error", - "noFallthroughSwitchClause": "error", - "noFunctionAssign": "error", - "noGlobalAssign": "error", - "noImportAssign": "error", - "noMisleadingCharacterClass": "error", - "noMisleadingInstantiator": "error", - "noPrototypeBuiltins": "error", - "noRedeclare": "error", - "noShadowRestrictedNames": "error", - "noUnsafeDeclarationMerging": "error", - "noUnsafeNegation": "error", - "useGetterReturn": "error", - "useValidTypeof": "error" - } - }, - "ignore": [ - "**/.eslintrc.cjs", - "**/node_modules", - "./built", - "./.eslintrc.js", - "./@types/**/*" - ] - }, - "overrides": [ - { - "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], - "linter": { - "rules": { - "correctness": { - "noConstAssign": "off", - "noGlobalObjectCalls": "off", - "noInvalidConstructorSuper": "off", - "noInvalidNewBuiltin": "off", - "noNewSymbol": "off", - "noSetterReturn": "off", - "noUndeclaredVariables": "off", - "noUnreachable": "off", - "noUnreachableSuper": "off" - }, - "style": { - "noArguments": "error", - "noVar": "error", - "useConst": "error" - }, - "suspicious": { - "noDuplicateClassMembers": "off", - "noDuplicateObjectKeys": "off", - "noDuplicateParameters": "off", - "noFunctionAssign": "off", - "noImportAssign": "off", - "noRedeclare": "off", - "noUnsafeNegation": "off", - "useGetterReturn": "off" - } - } - } - } - ] -} diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/check_connect.js similarity index 65% rename from packages/backend/scripts/check_connect.js rename to packages/backend/check_connect.js index ba25fd416c..0833da39f0 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/check_connect.js @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import Redis from 'ioredis'; -import { loadConfig } from '../built/config.js'; +import { loadConfig } from './built/config.js'; const config = loadConfig(); const redis = new Redis(config.redis); diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js deleted file mode 100644 index 4fd9f0cd51..0000000000 --- a/packages/backend/eslint.config.js +++ /dev/null @@ -1,46 +0,0 @@ -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'], - }, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json', './test/tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - 'import/order': ['warn', { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - 'object', - 'type', - ], - pathGroups: [{ - pattern: '@/**', - group: 'external', - position: 'after', - }], - }], - 'no-restricted-globals': ['error', { - name: '__dirname', - message: 'Not in ESModule. Use `import.meta.url` instead.', - }, { - name: '__filename', - message: 'Not in ESModule. Use `import.meta.url` instead.', - }], - }, - }, -]; diff --git a/packages/backend/generate_api_json.js b/packages/backend/generate_api_json.js new file mode 100644 index 0000000000..5819c60a5f --- /dev/null +++ b/packages/backend/generate_api_json.js @@ -0,0 +1,8 @@ +import { loadConfig } from './built/config.js' +import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js' +import { writeFileSync } from "node:fs"; + +const config = loadConfig(); +const spec = genOpenapiSpec(config); + +writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); \ No newline at end of file diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 5a4aa4e15a..97d777c862 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -160,6 +160,7 @@ module.exports = { testMatch: [ "/test/unit/**/*.ts", "/src/**/*.test.ts", + "/test/e2e/**/*.ts", ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped diff --git a/packages/backend/jest.config.e2e.cjs b/packages/backend/jest.config.e2e.cjs deleted file mode 100644 index 4502da47df..0000000000 --- a/packages/backend/jest.config.e2e.cjs +++ /dev/null @@ -1,15 +0,0 @@ -/* -* For a detailed explanation regarding each configuration property and type check, visit: -* https://jestjs.io/docs/en/configuration.html -*/ - -const base = require('./jest.config.cjs') - -module.exports = { - ...base, - globalSetup: "/built-test/entry.js", - setupFilesAfterEnv: ["/test/jest.setup.ts"], - testMatch: [ - "/test/e2e/**/*.ts", - ], -}; diff --git a/packages/backend/jest.config.unit.cjs b/packages/backend/jest.config.unit.cjs deleted file mode 100644 index aa5992936b..0000000000 --- a/packages/backend/jest.config.unit.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* -* For a detailed explanation regarding each configuration property and type check, visit: -* https://jestjs.io/docs/en/configuration.html -*/ - -const base = require('./jest.config.cjs') - -module.exports = { - ...base, - testMatch: [ - "/test/unit/**/*.ts", - "/src/**/*.test.ts", - ], -}; diff --git a/packages/backend/migration/1000000000000-Init.js b/packages/backend/migration/1000000000000-Init.js index c06885fd40..3da4329c1e 100644 --- a/packages/backend/migration/1000000000000-Init.js +++ b/packages/backend/migration/1000000000000-Init.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1556348509290-Pages.js b/packages/backend/migration/1556348509290-Pages.js index c7542e808c..55b21ceabb 100644 --- a/packages/backend/migration/1556348509290-Pages.js +++ b/packages/backend/migration/1556348509290-Pages.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1556746559567-UserProfile.js b/packages/backend/migration/1556746559567-UserProfile.js index 13ff6ce6bf..a058fd23c8 100644 --- a/packages/backend/migration/1556746559567-UserProfile.js +++ b/packages/backend/migration/1556746559567-UserProfile.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1557476068003-PinnedUsers.js b/packages/backend/migration/1557476068003-PinnedUsers.js index f2f1deae2f..6ec3f5ab16 100644 --- a/packages/backend/migration/1557476068003-PinnedUsers.js +++ b/packages/backend/migration/1557476068003-PinnedUsers.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1557761316509-AddSomeUrls.js b/packages/backend/migration/1557761316509-AddSomeUrls.js index 8632354a8d..aac0e668f1 100644 --- a/packages/backend/migration/1557761316509-AddSomeUrls.js +++ b/packages/backend/migration/1557761316509-AddSomeUrls.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1557932705754-ObjectStorageSetting.js b/packages/backend/migration/1557932705754-ObjectStorageSetting.js index 0e1ef321ab..6a241687c8 100644 --- a/packages/backend/migration/1557932705754-ObjectStorageSetting.js +++ b/packages/backend/migration/1557932705754-ObjectStorageSetting.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1558072954435-PageLike.js b/packages/backend/migration/1558072954435-PageLike.js index a08f68a0e6..395cf765d2 100644 --- a/packages/backend/migration/1558072954435-PageLike.js +++ b/packages/backend/migration/1558072954435-PageLike.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1558103093633-UserGroup.js b/packages/backend/migration/1558103093633-UserGroup.js index f762dc2371..2fe9284db5 100644 --- a/packages/backend/migration/1558103093633-UserGroup.js +++ b/packages/backend/migration/1558103093633-UserGroup.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1558257926829-UserGroupInvite.js b/packages/backend/migration/1558257926829-UserGroupInvite.js index 853b52d17d..4fe846cfd2 100644 --- a/packages/backend/migration/1558257926829-UserGroupInvite.js +++ b/packages/backend/migration/1558257926829-UserGroupInvite.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1558266512381-UserListJoining.js b/packages/backend/migration/1558266512381-UserListJoining.js index e161d52f12..e1ffb45e7f 100644 --- a/packages/backend/migration/1558266512381-UserListJoining.js +++ b/packages/backend/migration/1558266512381-UserListJoining.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1561706992953-webauthn.js b/packages/backend/migration/1561706992953-webauthn.js index 4c81035ff1..73d7c46cdd 100644 --- a/packages/backend/migration/1561706992953-webauthn.js +++ b/packages/backend/migration/1561706992953-webauthn.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1561873850023-ChartIndexes.js b/packages/backend/migration/1561873850023-ChartIndexes.js index 3f190ce143..34c5a333fc 100644 --- a/packages/backend/migration/1561873850023-ChartIndexes.js +++ b/packages/backend/migration/1561873850023-ChartIndexes.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1562422242907-PasswordLessLogin.js b/packages/backend/migration/1562422242907-PasswordLessLogin.js index 4c0fbbbc9f..717b9f70d8 100644 --- a/packages/backend/migration/1562422242907-PasswordLessLogin.js +++ b/packages/backend/migration/1562422242907-PasswordLessLogin.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1562444565093-PinnedPage.js b/packages/backend/migration/1562444565093-PinnedPage.js index 89639399f0..d86406b6b2 100644 --- a/packages/backend/migration/1562444565093-PinnedPage.js +++ b/packages/backend/migration/1562444565093-PinnedPage.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1562448332510-PageTitleHideOption.js b/packages/backend/migration/1562448332510-PageTitleHideOption.js index 70d54aa777..5415c0c401 100644 --- a/packages/backend/migration/1562448332510-PageTitleHideOption.js +++ b/packages/backend/migration/1562448332510-PageTitleHideOption.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1562869971568-ModerationLog.js b/packages/backend/migration/1562869971568-ModerationLog.js index 3dd9b22edf..1092fbdf8a 100644 --- a/packages/backend/migration/1562869971568-ModerationLog.js +++ b/packages/backend/migration/1562869971568-ModerationLog.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1563757595828-UsedUsername.js b/packages/backend/migration/1563757595828-UsedUsername.js index 258e5abab2..6771225c07 100644 --- a/packages/backend/migration/1563757595828-UsedUsername.js +++ b/packages/backend/migration/1563757595828-UsedUsername.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1565634203341-room.js b/packages/backend/migration/1565634203341-room.js index 04c9749c1b..ee1dc064ab 100644 --- a/packages/backend/migration/1565634203341-room.js +++ b/packages/backend/migration/1565634203341-room.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1571220798684-CustomEmojiCategory.js b/packages/backend/migration/1571220798684-CustomEmojiCategory.js index 1fc78a65ff..a851caf8a3 100644 --- a/packages/backend/migration/1571220798684-CustomEmojiCategory.js +++ b/packages/backend/migration/1571220798684-CustomEmojiCategory.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1572760203493-nodeinfo.js b/packages/backend/migration/1572760203493-nodeinfo.js index ea7a67bc3e..317ff8a1ec 100644 --- a/packages/backend/migration/1572760203493-nodeinfo.js +++ b/packages/backend/migration/1572760203493-nodeinfo.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1576269851876-TalkFederationId.js b/packages/backend/migration/1576269851876-TalkFederationId.js index c49c716e7a..b63e10b098 100644 --- a/packages/backend/migration/1576269851876-TalkFederationId.js +++ b/packages/backend/migration/1576269851876-TalkFederationId.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js index 192dbe3485..a505ffce31 100644 --- a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js +++ b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1579267006611-v12.js b/packages/backend/migration/1579267006611-v12.js index 9267be5630..6c57321bc5 100644 --- a/packages/backend/migration/1579267006611-v12.js +++ b/packages/backend/migration/1579267006611-v12.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1579270193251-v12-2.js b/packages/backend/migration/1579270193251-v12-2.js index e2ca9709ea..694101fe0b 100644 --- a/packages/backend/migration/1579270193251-v12-2.js +++ b/packages/backend/migration/1579270193251-v12-2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1579282808087-v12-3.js b/packages/backend/migration/1579282808087-v12-3.js index 4098f041c8..25c5add6cd 100644 --- a/packages/backend/migration/1579282808087-v12-3.js +++ b/packages/backend/migration/1579282808087-v12-3.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1579544426412-v12-4.js b/packages/backend/migration/1579544426412-v12-4.js index 1153993f35..972b844507 100644 --- a/packages/backend/migration/1579544426412-v12-4.js +++ b/packages/backend/migration/1579544426412-v12-4.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1579977526288-v12-5.js b/packages/backend/migration/1579977526288-v12-5.js index d9e1b48bb2..c48f1138a8 100644 --- a/packages/backend/migration/1579977526288-v12-5.js +++ b/packages/backend/migration/1579977526288-v12-5.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1579993013959-v12-6.js b/packages/backend/migration/1579993013959-v12-6.js index 9c249422a2..d1ee3ec51e 100644 --- a/packages/backend/migration/1579993013959-v12-6.js +++ b/packages/backend/migration/1579993013959-v12-6.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1580069531114-v12-7.js b/packages/backend/migration/1580069531114-v12-7.js index ceee6b2031..e560630ea3 100644 --- a/packages/backend/migration/1580069531114-v12-7.js +++ b/packages/backend/migration/1580069531114-v12-7.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1580148575182-v12-8.js b/packages/backend/migration/1580148575182-v12-8.js index 6841dcc38f..32da3c99b9 100644 --- a/packages/backend/migration/1580148575182-v12-8.js +++ b/packages/backend/migration/1580148575182-v12-8.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1580154400017-v12-9.js b/packages/backend/migration/1580154400017-v12-9.js index c01d8089d0..12c69b4eda 100644 --- a/packages/backend/migration/1580154400017-v12-9.js +++ b/packages/backend/migration/1580154400017-v12-9.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1580276619901-v12-10.js b/packages/backend/migration/1580276619901-v12-10.js index be6e467fab..265d5366b1 100644 --- a/packages/backend/migration/1580276619901-v12-10.js +++ b/packages/backend/migration/1580276619901-v12-10.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1580331224276-v12-11.js b/packages/backend/migration/1580331224276-v12-11.js index af817a8c8a..d3a00e3d61 100644 --- a/packages/backend/migration/1580331224276-v12-11.js +++ b/packages/backend/migration/1580331224276-v12-11.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1580508795118-v12-12.js b/packages/backend/migration/1580508795118-v12-12.js index 4bd855f7ab..65ddc64327 100644 --- a/packages/backend/migration/1580508795118-v12-12.js +++ b/packages/backend/migration/1580508795118-v12-12.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1580543501339-v12-13.js b/packages/backend/migration/1580543501339-v12-13.js index be76c02163..6804a715b8 100644 --- a/packages/backend/migration/1580543501339-v12-13.js +++ b/packages/backend/migration/1580543501339-v12-13.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1580864313253-v12-14.js b/packages/backend/migration/1580864313253-v12-14.js index f8891a2b66..4de5a46b6a 100644 --- a/packages/backend/migration/1580864313253-v12-14.js +++ b/packages/backend/migration/1580864313253-v12-14.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1581526429287-user-group-invitation.js b/packages/backend/migration/1581526429287-user-group-invitation.js index 51703e2ba1..e17110edd3 100644 --- a/packages/backend/migration/1581526429287-user-group-invitation.js +++ b/packages/backend/migration/1581526429287-user-group-invitation.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1581695816408-user-group-antenna.js b/packages/backend/migration/1581695816408-user-group-antenna.js index e6791ba1a4..9e928351e1 100644 --- a/packages/backend/migration/1581695816408-user-group-antenna.js +++ b/packages/backend/migration/1581695816408-user-group-antenna.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js index 28ce4cc142..034c0ddf1f 100644 --- a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js +++ b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1581979837262-promo.js b/packages/backend/migration/1581979837262-promo.js index 707c85fcb3..2eff241b8e 100644 --- a/packages/backend/migration/1581979837262-promo.js +++ b/packages/backend/migration/1581979837262-promo.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1582019042083-featured-injecttion.js b/packages/backend/migration/1582019042083-featured-injecttion.js index f308f0a454..5c7cd5f35e 100644 --- a/packages/backend/migration/1582019042083-featured-injecttion.js +++ b/packages/backend/migration/1582019042083-featured-injecttion.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1582210532752-antenna-exclude.js b/packages/backend/migration/1582210532752-antenna-exclude.js index 9b87e3ff39..b1e4f7ff59 100644 --- a/packages/backend/migration/1582210532752-antenna-exclude.js +++ b/packages/backend/migration/1582210532752-antenna-exclude.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1582875306439-note-reaction-length.js b/packages/backend/migration/1582875306439-note-reaction-length.js index e801d1ac44..84c10d069d 100644 --- a/packages/backend/migration/1582875306439-note-reaction-length.js +++ b/packages/backend/migration/1582875306439-note-reaction-length.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1585361548360-miauth.js b/packages/backend/migration/1585361548360-miauth.js index d5932c6083..a53fa2c740 100644 --- a/packages/backend/migration/1585361548360-miauth.js +++ b/packages/backend/migration/1585361548360-miauth.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1585385921215-custom-notification.js b/packages/backend/migration/1585385921215-custom-notification.js index 35303b99e9..110bbe5656 100644 --- a/packages/backend/migration/1585385921215-custom-notification.js +++ b/packages/backend/migration/1585385921215-custom-notification.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1585772678853-ap-url.js b/packages/backend/migration/1585772678853-ap-url.js index f978fc80b4..325e6d88fc 100644 --- a/packages/backend/migration/1585772678853-ap-url.js +++ b/packages/backend/migration/1585772678853-ap-url.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js index fde8629bba..99a9ab18ed 100644 --- a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js +++ b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1586641139527-remote-reaction.js b/packages/backend/migration/1586641139527-remote-reaction.js index 3e907af5f1..abc290b946 100644 --- a/packages/backend/migration/1586641139527-remote-reaction.js +++ b/packages/backend/migration/1586641139527-remote-reaction.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1586708940386-pageAiScript.js b/packages/backend/migration/1586708940386-pageAiScript.js index ce5007cea1..49fba95c51 100644 --- a/packages/backend/migration/1586708940386-pageAiScript.js +++ b/packages/backend/migration/1586708940386-pageAiScript.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1588044505511-hCaptcha.js b/packages/backend/migration/1588044505511-hCaptcha.js index aeacb653b3..271039d878 100644 --- a/packages/backend/migration/1588044505511-hCaptcha.js +++ b/packages/backend/migration/1588044505511-hCaptcha.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1589023282116-pubRelay.js b/packages/backend/migration/1589023282116-pubRelay.js index 8739adb733..4e9f2824c9 100644 --- a/packages/backend/migration/1589023282116-pubRelay.js +++ b/packages/backend/migration/1589023282116-pubRelay.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1595075960584-blurhash.js b/packages/backend/migration/1595075960584-blurhash.js index 9752625cd2..3f390ce64d 100644 --- a/packages/backend/migration/1595075960584-blurhash.js +++ b/packages/backend/migration/1595075960584-blurhash.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js index fdff8c633a..14ac8bf455 100644 --- a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js +++ b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1595676934834-instance-icon-url.js b/packages/backend/migration/1595676934834-instance-icon-url.js index 5f834064c4..f849d5b2c3 100644 --- a/packages/backend/migration/1595676934834-instance-icon-url.js +++ b/packages/backend/migration/1595676934834-instance-icon-url.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1595771249699-word-mute.js b/packages/backend/migration/1595771249699-word-mute.js index f4fa1227e3..50c376cdd1 100644 --- a/packages/backend/migration/1595771249699-word-mute.js +++ b/packages/backend/migration/1595771249699-word-mute.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1595782306083-word-mute2.js b/packages/backend/migration/1595782306083-word-mute2.js index 3c2062ec07..fb273c5fbb 100644 --- a/packages/backend/migration/1595782306083-word-mute2.js +++ b/packages/backend/migration/1595782306083-word-mute2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1596548170836-channel.js b/packages/backend/migration/1596548170836-channel.js index ee6753a476..99fb134f48 100644 --- a/packages/backend/migration/1596548170836-channel.js +++ b/packages/backend/migration/1596548170836-channel.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1596786425167-channel2.js b/packages/backend/migration/1596786425167-channel2.js index 9e6ead4378..717fef8eb6 100644 --- a/packages/backend/migration/1596786425167-channel2.js +++ b/packages/backend/migration/1596786425167-channel2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js index bc32d4a052..b7611eb2c1 100644 --- a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js +++ b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js index 99686bd70e..a655f1fd09 100644 --- a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js +++ b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1597385880794-add-sensitive-index.js b/packages/backend/migration/1597385880794-add-sensitive-index.js index a67810880b..403aa72f11 100644 --- a/packages/backend/migration/1597385880794-add-sensitive-index.js +++ b/packages/backend/migration/1597385880794-add-sensitive-index.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1597459042300-channel-unread.js b/packages/backend/migration/1597459042300-channel-unread.js index ced9b5265a..a87c2f811e 100644 --- a/packages/backend/migration/1597459042300-channel-unread.js +++ b/packages/backend/migration/1597459042300-channel-unread.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js index ca4eba385e..f3cb696d00 100644 --- a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js +++ b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1600353287890-mutingNotificationTypes.js b/packages/backend/migration/1600353287890-mutingNotificationTypes.js index 0996aa21f6..c11db226d1 100644 --- a/packages/backend/migration/1600353287890-mutingNotificationTypes.js +++ b/packages/backend/migration/1600353287890-mutingNotificationTypes.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1603094348345-refine-abuse-user-report.js b/packages/backend/migration/1603094348345-refine-abuse-user-report.js index 354915b165..f706ebbe2f 100644 --- a/packages/backend/migration/1603094348345-refine-abuse-user-report.js +++ b/packages/backend/migration/1603094348345-refine-abuse-user-report.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js index 75dd3513b5..3d350125d1 100644 --- a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js +++ b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1603776877564-instance-theme-color.js b/packages/backend/migration/1603776877564-instance-theme-color.js index c8ab89ab56..36714f412f 100644 --- a/packages/backend/migration/1603776877564-instance-theme-color.js +++ b/packages/backend/migration/1603776877564-instance-theme-color.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1603781553011-instance-favicon.js b/packages/backend/migration/1603781553011-instance-favicon.js index 7d793d4f1f..5280980007 100644 --- a/packages/backend/migration/1603781553011-instance-favicon.js +++ b/packages/backend/migration/1603781553011-instance-favicon.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1604821689616-delete-auto-watch.js b/packages/backend/migration/1604821689616-delete-auto-watch.js index 8160877038..7a466a0387 100644 --- a/packages/backend/migration/1604821689616-delete-auto-watch.js +++ b/packages/backend/migration/1604821689616-delete-auto-watch.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1605408848373-clip-description.js b/packages/backend/migration/1605408848373-clip-description.js index 77a218791c..604188a546 100644 --- a/packages/backend/migration/1605408848373-clip-description.js +++ b/packages/backend/migration/1605408848373-clip-description.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1605408971051-comments.js b/packages/backend/migration/1605408971051-comments.js index 494bfb7950..e9682288c3 100644 --- a/packages/backend/migration/1605408971051-comments.js +++ b/packages/backend/migration/1605408971051-comments.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1605585339718-instance-pinned-pages.js b/packages/backend/migration/1605585339718-instance-pinned-pages.js index 15a0cecd19..3420f0cc13 100644 --- a/packages/backend/migration/1605585339718-instance-pinned-pages.js +++ b/packages/backend/migration/1605585339718-instance-pinned-pages.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1605965516823-instance-images.js b/packages/backend/migration/1605965516823-instance-images.js index 9cc2eb4032..e5b2306370 100644 --- a/packages/backend/migration/1605965516823-instance-images.js +++ b/packages/backend/migration/1605965516823-instance-images.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1606191203881-no-crawle.js b/packages/backend/migration/1606191203881-no-crawle.js index af04566eaa..095197ede3 100644 --- a/packages/backend/migration/1606191203881-no-crawle.js +++ b/packages/backend/migration/1606191203881-no-crawle.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1607151207216-instance-pinned-clip.js b/packages/backend/migration/1607151207216-instance-pinned-clip.js index f85c3d42d7..6ad2a594c3 100644 --- a/packages/backend/migration/1607151207216-instance-pinned-clip.js +++ b/packages/backend/migration/1607151207216-instance-pinned-clip.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1607353487793-isExplorable.js b/packages/backend/migration/1607353487793-isExplorable.js index e07fe6c306..1c867345d1 100644 --- a/packages/backend/migration/1607353487793-isExplorable.js +++ b/packages/backend/migration/1607353487793-isExplorable.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1610277136869-registry.js b/packages/backend/migration/1610277136869-registry.js index 1a10f23590..8cf9ad8156 100644 --- a/packages/backend/migration/1610277136869-registry.js +++ b/packages/backend/migration/1610277136869-registry.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1610277585759-registry2.js b/packages/backend/migration/1610277585759-registry2.js index 46e56279f4..f04f264fae 100644 --- a/packages/backend/migration/1610277585759-registry2.js +++ b/packages/backend/migration/1610277585759-registry2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1610283021566-registry3.js b/packages/backend/migration/1610283021566-registry3.js index 402040f38b..d2f78a7a0d 100644 --- a/packages/backend/migration/1610283021566-registry3.js +++ b/packages/backend/migration/1610283021566-registry3.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1611354329133-followersUri.js b/packages/backend/migration/1611354329133-followersUri.js index 15abb2a9d1..4b494027de 100644 --- a/packages/backend/migration/1611354329133-followersUri.js +++ b/packages/backend/migration/1611354329133-followersUri.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1611397665007-gallery.js b/packages/backend/migration/1611397665007-gallery.js index cbd2b62c56..926e8492d7 100644 --- a/packages/backend/migration/1611397665007-gallery.js +++ b/packages/backend/migration/1611397665007-gallery.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js index c5440b7a48..d036bd600f 100644 --- a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js +++ b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1612619156584-announcement-email.js b/packages/backend/migration/1612619156584-announcement-email.js index ddacab322b..02bd0063ee 100644 --- a/packages/backend/migration/1612619156584-announcement-email.js +++ b/packages/backend/migration/1612619156584-announcement-email.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1613155914446-emailNotificationTypes.js b/packages/backend/migration/1613155914446-emailNotificationTypes.js index d34ba7e826..47e5cecf44 100644 --- a/packages/backend/migration/1613155914446-emailNotificationTypes.js +++ b/packages/backend/migration/1613155914446-emailNotificationTypes.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1613181457597-user-lang.js b/packages/backend/migration/1613181457597-user-lang.js index 6ef5245953..0ba2d3477e 100644 --- a/packages/backend/migration/1613181457597-user-lang.js +++ b/packages/backend/migration/1613181457597-user-lang.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js index 8529ea3247..fad0e49b48 100644 --- a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js +++ b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1615965918224-chart-v2.js b/packages/backend/migration/1615965918224-chart-v2.js index deecde7227..06f796e6f0 100644 --- a/packages/backend/migration/1615965918224-chart-v2.js +++ b/packages/backend/migration/1615965918224-chart-v2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1615966519402-chart-v2-2.js b/packages/backend/migration/1615966519402-chart-v2-2.js index 7842a27108..8450d96793 100644 --- a/packages/backend/migration/1615966519402-chart-v2-2.js +++ b/packages/backend/migration/1615966519402-chart-v2-2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1618637372000-user-last-active-date.js b/packages/backend/migration/1618637372000-user-last-active-date.js index 7caf179fa5..859721bb65 100644 --- a/packages/backend/migration/1618637372000-user-last-active-date.js +++ b/packages/backend/migration/1618637372000-user-last-active-date.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1618639857000-user-hide-online-status.js b/packages/backend/migration/1618639857000-user-hide-online-status.js index 2012962742..3eb73d44e9 100644 --- a/packages/backend/migration/1618639857000-user-hide-online-status.js +++ b/packages/backend/migration/1618639857000-user-hide-online-status.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1619942102890-password-reset.js b/packages/backend/migration/1619942102890-password-reset.js index 7784da2bce..c02736f706 100644 --- a/packages/backend/migration/1619942102890-password-reset.js +++ b/packages/backend/migration/1619942102890-password-reset.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1620019354680-ad.js b/packages/backend/migration/1620019354680-ad.js index 7630ed01a1..2502b885e4 100644 --- a/packages/backend/migration/1620019354680-ad.js +++ b/packages/backend/migration/1620019354680-ad.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1620364649428-ad2.js b/packages/backend/migration/1620364649428-ad2.js index 7959185685..36c9d3c7e6 100644 --- a/packages/backend/migration/1620364649428-ad2.js +++ b/packages/backend/migration/1620364649428-ad2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1621479946000-add-note-indexes.js b/packages/backend/migration/1621479946000-add-note-indexes.js index f72bf8211e..80756f63df 100644 --- a/packages/backend/migration/1621479946000-add-note-indexes.js +++ b/packages/backend/migration/1621479946000-add-note-indexes.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1622679304522-user-profile-description-length.js b/packages/backend/migration/1622679304522-user-profile-description-length.js index 7324175b46..4bbf92bfc5 100644 --- a/packages/backend/migration/1622679304522-user-profile-description-length.js +++ b/packages/backend/migration/1622679304522-user-profile-description-length.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1622681548499-log-message-length.js b/packages/backend/migration/1622681548499-log-message-length.js index b4d8d497e3..0c15314113 100644 --- a/packages/backend/migration/1622681548499-log-message-length.js +++ b/packages/backend/migration/1622681548499-log-message-length.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1626509500668-fix-remote-file-proxy.js b/packages/backend/migration/1626509500668-fix-remote-file-proxy.js index 9145247ab1..1e870ee02e 100644 --- a/packages/backend/migration/1626509500668-fix-remote-file-proxy.js +++ b/packages/backend/migration/1626509500668-fix-remote-file-proxy.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1629004542760-chart-reindex.js b/packages/backend/migration/1629004542760-chart-reindex.js index 072cdec3c1..a95d49ef19 100644 --- a/packages/backend/migration/1629004542760-chart-reindex.js +++ b/packages/backend/migration/1629004542760-chart-reindex.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1629024377804-deepl-integration.js b/packages/backend/migration/1629024377804-deepl-integration.js index 5889196f15..b5ef2f0856 100644 --- a/packages/backend/migration/1629024377804-deepl-integration.js +++ b/packages/backend/migration/1629024377804-deepl-integration.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1629288472000-fix-channel-userId.js b/packages/backend/migration/1629288472000-fix-channel-userId.js index d7907d05bd..3201461acc 100644 --- a/packages/backend/migration/1629288472000-fix-channel-userId.js +++ b/packages/backend/migration/1629288472000-fix-channel-userId.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1629387925000-disableRightClick.js b/packages/backend/migration/1629387925000-disableRightClick.js index 6c821791fb..a4f9da6d52 100644 --- a/packages/backend/migration/1629387925000-disableRightClick.js +++ b/packages/backend/migration/1629387925000-disableRightClick.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1629512953000-user-is-deleted.js b/packages/backend/migration/1629512953000-user-is-deleted.js index 94165e466b..7b04ffaa7b 100644 --- a/packages/backend/migration/1629512953000-user-is-deleted.js +++ b/packages/backend/migration/1629512953000-user-is-deleted.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1629778475000-deepl-integration2.js b/packages/backend/migration/1629778475000-deepl-integration2.js index a54daf8fb3..3a58bfd341 100644 --- a/packages/backend/migration/1629778475000-deepl-integration2.js +++ b/packages/backend/migration/1629778475000-deepl-integration2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1629833361000-AddShowTLReplies.js b/packages/backend/migration/1629833361000-AddShowTLReplies.js index b80e2ef67f..5e5c2228bb 100644 --- a/packages/backend/migration/1629833361000-AddShowTLReplies.js +++ b/packages/backend/migration/1629833361000-AddShowTLReplies.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1629968054000_userInstanceBlocks.js b/packages/backend/migration/1629968054000_userInstanceBlocks.js index e88fa8aece..b9afec28e5 100644 --- a/packages/backend/migration/1629968054000_userInstanceBlocks.js +++ b/packages/backend/migration/1629968054000_userInstanceBlocks.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1633068642000-email-required-for-signup.js b/packages/backend/migration/1633068642000-email-required-for-signup.js index d23db2052f..e0d623eac6 100644 --- a/packages/backend/migration/1633068642000-email-required-for-signup.js +++ b/packages/backend/migration/1633068642000-email-required-for-signup.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1633071909016-user-pending.js b/packages/backend/migration/1633071909016-user-pending.js index db0f2fde1a..cb92c33af4 100644 --- a/packages/backend/migration/1633071909016-user-pending.js +++ b/packages/backend/migration/1633071909016-user-pending.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1634486652000-user-public-reactions.js b/packages/backend/migration/1634486652000-user-public-reactions.js index ce1818886a..36cd619963 100644 --- a/packages/backend/migration/1634486652000-user-public-reactions.js +++ b/packages/backend/migration/1634486652000-user-public-reactions.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1634902659689-delete-log.js b/packages/backend/migration/1634902659689-delete-log.js index 2e2267f9f4..01442e6f45 100644 --- a/packages/backend/migration/1634902659689-delete-log.js +++ b/packages/backend/migration/1634902659689-delete-log.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1635500777168-note-thread-mute.js b/packages/backend/migration/1635500777168-note-thread-mute.js index d5fca59594..14d24e4748 100644 --- a/packages/backend/migration/1635500777168-note-thread-mute.js +++ b/packages/backend/migration/1635500777168-note-thread-mute.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1636197624383-ff-visibility.js b/packages/backend/migration/1636197624383-ff-visibility.js index 27faae1c92..e6b573d9c6 100644 --- a/packages/backend/migration/1636197624383-ff-visibility.js +++ b/packages/backend/migration/1636197624383-ff-visibility.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1636697408073-remove-via-mobile.js b/packages/backend/migration/1636697408073-remove-via-mobile.js index 81f0b63443..120d4242c8 100644 --- a/packages/backend/migration/1636697408073-remove-via-mobile.js +++ b/packages/backend/migration/1636697408073-remove-via-mobile.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1637320813000-forwarded-report.js b/packages/backend/migration/1637320813000-forwarded-report.js index 8125468aae..97060689ff 100644 --- a/packages/backend/migration/1637320813000-forwarded-report.js +++ b/packages/backend/migration/1637320813000-forwarded-report.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1639325650583-chart-v3.js b/packages/backend/migration/1639325650583-chart-v3.js index 2255476394..406fe64f1f 100644 --- a/packages/backend/migration/1639325650583-chart-v3.js +++ b/packages/backend/migration/1639325650583-chart-v3.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1642611822809-emoji-url.js b/packages/backend/migration/1642611822809-emoji-url.js index 421614b408..499a526431 100644 --- a/packages/backend/migration/1642611822809-emoji-url.js +++ b/packages/backend/migration/1642611822809-emoji-url.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js index e61a3fc49e..e3167e4d92 100644 --- a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js +++ b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1643963705770-chart-v4.js b/packages/backend/migration/1643963705770-chart-v4.js index 77355cd7f3..bda6a9a6f5 100644 --- a/packages/backend/migration/1643963705770-chart-v4.js +++ b/packages/backend/migration/1643963705770-chart-v4.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1643966656277-chart-v5.js b/packages/backend/migration/1643966656277-chart-v5.js index 54e4705e56..753360eef4 100644 --- a/packages/backend/migration/1643966656277-chart-v5.js +++ b/packages/backend/migration/1643966656277-chart-v5.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1643967331284-chart-v6.js b/packages/backend/migration/1643967331284-chart-v6.js index aa64bc9faa..5e24a455b1 100644 --- a/packages/backend/migration/1643967331284-chart-v6.js +++ b/packages/backend/migration/1643967331284-chart-v6.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644010796173-convert-hard-mutes.js b/packages/backend/migration/1644010796173-convert-hard-mutes.js index 9aec21b5ff..8dd209f935 100644 --- a/packages/backend/migration/1644010796173-convert-hard-mutes.js +++ b/packages/backend/migration/1644010796173-convert-hard-mutes.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644058404077-chart-v7.js b/packages/backend/migration/1644058404077-chart-v7.js index a09fff1bc7..a73fb26653 100644 --- a/packages/backend/migration/1644058404077-chart-v7.js +++ b/packages/backend/migration/1644058404077-chart-v7.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644059847460-chart-v8.js b/packages/backend/migration/1644059847460-chart-v8.js index 43b95926b6..c71db147b3 100644 --- a/packages/backend/migration/1644059847460-chart-v8.js +++ b/packages/backend/migration/1644059847460-chart-v8.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644060125705-chart-v9.js b/packages/backend/migration/1644060125705-chart-v9.js index dc99f3c8f8..404e575b37 100644 --- a/packages/backend/migration/1644060125705-chart-v9.js +++ b/packages/backend/migration/1644060125705-chart-v9.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644073149413-chart-v10.js b/packages/backend/migration/1644073149413-chart-v10.js index 4d36235729..e0f15a2f09 100644 --- a/packages/backend/migration/1644073149413-chart-v10.js +++ b/packages/backend/migration/1644073149413-chart-v10.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644095659741-chart-v11.js b/packages/backend/migration/1644095659741-chart-v11.js index 80bacbf710..8a353dc07b 100644 --- a/packages/backend/migration/1644095659741-chart-v11.js +++ b/packages/backend/migration/1644095659741-chart-v11.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644328606241-chart-v12.js b/packages/backend/migration/1644328606241-chart-v12.js index 15c0dd9040..e93f7e6bf5 100644 --- a/packages/backend/migration/1644328606241-chart-v12.js +++ b/packages/backend/migration/1644328606241-chart-v12.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644331238153-chart-v13.js b/packages/backend/migration/1644331238153-chart-v13.js index 0c2db66f27..c1c7eeba98 100644 --- a/packages/backend/migration/1644331238153-chart-v13.js +++ b/packages/backend/migration/1644331238153-chart-v13.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644344266289-chart-v14.js b/packages/backend/migration/1644344266289-chart-v14.js index 0f4688ab77..04575669e9 100644 --- a/packages/backend/migration/1644344266289-chart-v14.js +++ b/packages/backend/migration/1644344266289-chart-v14.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644395759931-instance-theme-color.js b/packages/backend/migration/1644395759931-instance-theme-color.js index fd7356e68a..fdac63dded 100644 --- a/packages/backend/migration/1644395759931-instance-theme-color.js +++ b/packages/backend/migration/1644395759931-instance-theme-color.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644481657998-chart-v15.js b/packages/backend/migration/1644481657998-chart-v15.js index 964bea3d07..4543b04e3b 100644 --- a/packages/backend/migration/1644481657998-chart-v15.js +++ b/packages/backend/migration/1644481657998-chart-v15.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1644551208096-following-indexes.js b/packages/backend/migration/1644551208096-following-indexes.js index 8d1d4890dc..cfea976a94 100644 --- a/packages/backend/migration/1644551208096-following-indexes.js +++ b/packages/backend/migration/1644551208096-following-indexes.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1645340161439-remove-max-note-text-length.js b/packages/backend/migration/1645340161439-remove-max-note-text-length.js index 1cf6b0801b..d33fa23aba 100644 --- a/packages/backend/migration/1645340161439-remove-max-note-text-length.js +++ b/packages/backend/migration/1645340161439-remove-max-note-text-length.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1645599900873-federation-chart-pubsub.js b/packages/backend/migration/1645599900873-federation-chart-pubsub.js index 3042c8ecd9..3752557e06 100644 --- a/packages/backend/migration/1645599900873-federation-chart-pubsub.js +++ b/packages/backend/migration/1645599900873-federation-chart-pubsub.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1646143552768-instance-default-theme.js b/packages/backend/migration/1646143552768-instance-default-theme.js index 8f0755e3a2..73e6ccc5b9 100644 --- a/packages/backend/migration/1646143552768-instance-default-theme.js +++ b/packages/backend/migration/1646143552768-instance-default-theme.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1646387162108-mute-expires-at.js b/packages/backend/migration/1646387162108-mute-expires-at.js index 412db14881..4ab0c559f4 100644 --- a/packages/backend/migration/1646387162108-mute-expires-at.js +++ b/packages/backend/migration/1646387162108-mute-expires-at.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1646549089451-poll-ended-notification.js b/packages/backend/migration/1646549089451-poll-ended-notification.js index 6c481c6ac6..b908832071 100644 --- a/packages/backend/migration/1646549089451-poll-ended-notification.js +++ b/packages/backend/migration/1646549089451-poll-ended-notification.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1646633030285-chart-federation-active.js b/packages/backend/migration/1646633030285-chart-federation-active.js index 13d54c3180..be3f0ca7e5 100644 --- a/packages/backend/migration/1646633030285-chart-federation-active.js +++ b/packages/backend/migration/1646633030285-chart-federation-active.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1646655454495-remove-instance-drive-columns.js b/packages/backend/migration/1646655454495-remove-instance-drive-columns.js index 04d6fce887..d77cf74308 100644 --- a/packages/backend/migration/1646655454495-remove-instance-drive-columns.js +++ b/packages/backend/migration/1646655454495-remove-instance-drive-columns.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js b/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js index 289b929ad9..b4015e5eb4 100644 --- a/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js +++ b/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1648548247382-webhook.js b/packages/backend/migration/1648548247382-webhook.js index f31d3c5bb5..f275ec3c62 100644 --- a/packages/backend/migration/1648548247382-webhook.js +++ b/packages/backend/migration/1648548247382-webhook.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1648816172177-webhook-2.js b/packages/backend/migration/1648816172177-webhook-2.js index 4d1b293b2c..b31a90dd2b 100644 --- a/packages/backend/migration/1648816172177-webhook-2.js +++ b/packages/backend/migration/1648816172177-webhook-2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1651224615271-foreign-key.js b/packages/backend/migration/1651224615271-foreign-key.js index fa51bb5e31..16e40b7dac 100644 --- a/packages/backend/migration/1651224615271-foreign-key.js +++ b/packages/backend/migration/1651224615271-foreign-key.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1652859567549-uniform-themecolor.js b/packages/backend/migration/1652859567549-uniform-themecolor.js index 754e089824..e629c073b3 100644 --- a/packages/backend/migration/1652859567549-uniform-themecolor.js +++ b/packages/backend/migration/1652859567549-uniform-themecolor.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1654589815249-increase-smtp-char-limit.js b/packages/backend/migration/1654589815249-increase-smtp-char-limit.js index 265e859180..cdcffa98cd 100644 --- a/packages/backend/migration/1654589815249-increase-smtp-char-limit.js +++ b/packages/backend/migration/1654589815249-increase-smtp-char-limit.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1655368940105-nsfw-detection.js b/packages/backend/migration/1655368940105-nsfw-detection.js index d2d0d00117..02baccb8f6 100644 --- a/packages/backend/migration/1655368940105-nsfw-detection.js +++ b/packages/backend/migration/1655368940105-nsfw-detection.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1655371960534-nsfw-detection-2.js b/packages/backend/migration/1655371960534-nsfw-detection-2.js index e5adbddca4..5b41cf9bcf 100644 --- a/packages/backend/migration/1655371960534-nsfw-detection-2.js +++ b/packages/backend/migration/1655371960534-nsfw-detection-2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1655388169582-nsfw-detection-3.js b/packages/backend/migration/1655388169582-nsfw-detection-3.js index 12fc281327..f4a570bf4e 100644 --- a/packages/backend/migration/1655388169582-nsfw-detection-3.js +++ b/packages/backend/migration/1655388169582-nsfw-detection-3.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1655393015659-nsfw-detection-4.js b/packages/backend/migration/1655393015659-nsfw-detection-4.js index 39fb175679..99bd9e3af0 100644 --- a/packages/backend/migration/1655393015659-nsfw-detection-4.js +++ b/packages/backend/migration/1655393015659-nsfw-detection-4.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js index e64c8c1b82..55ea3ecaa9 100644 --- a/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js +++ b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1655918165614-user-ip.js b/packages/backend/migration/1655918165614-user-ip.js index 668c6d909b..65677770c0 100644 --- a/packages/backend/migration/1655918165614-user-ip.js +++ b/packages/backend/migration/1655918165614-user-ip.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1656122560740-file-ip.js b/packages/backend/migration/1656122560740-file-ip.js index e5efaf3d9f..f451cae7a0 100644 --- a/packages/backend/migration/1656122560740-file-ip.js +++ b/packages/backend/migration/1656122560740-file-ip.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1656251734807-nsfw-detection-5.js b/packages/backend/migration/1656251734807-nsfw-detection-5.js index 9b36bd76eb..7e0e709c45 100644 --- a/packages/backend/migration/1656251734807-nsfw-detection-5.js +++ b/packages/backend/migration/1656251734807-nsfw-detection-5.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1656328812281-ip-2.js b/packages/backend/migration/1656328812281-ip-2.js index 39fcd1d83d..453a3d01c5 100644 --- a/packages/backend/migration/1656328812281-ip-2.js +++ b/packages/backend/migration/1656328812281-ip-2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1656408772602-nsfw-detection-6.js b/packages/backend/migration/1656408772602-nsfw-detection-6.js index efadd22e5d..344261da4b 100644 --- a/packages/backend/migration/1656408772602-nsfw-detection-6.js +++ b/packages/backend/migration/1656408772602-nsfw-detection-6.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1656772790599-user-moderation-note.js b/packages/backend/migration/1656772790599-user-moderation-note.js index ef2f0f6522..a8c2442309 100644 --- a/packages/backend/migration/1656772790599-user-moderation-note.js +++ b/packages/backend/migration/1656772790599-user-moderation-note.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1657346559800-active-email-validation.js b/packages/backend/migration/1657346559800-active-email-validation.js index e8d5b29cdf..37a6cb04bc 100644 --- a/packages/backend/migration/1657346559800-active-email-validation.js +++ b/packages/backend/migration/1657346559800-active-email-validation.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1660183643857-multipleTranslationServices.js b/packages/backend/migration/1660183643857-multipleTranslationServices.js index 1ac7512cdf..e87ce073f2 100644 --- a/packages/backend/migration/1660183643857-multipleTranslationServices.js +++ b/packages/backend/migration/1660183643857-multipleTranslationServices.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1664694635394-turnstile.js b/packages/backend/migration/1664694635394-turnstile.js index a9baf4c657..52144c3ff1 100644 --- a/packages/backend/migration/1664694635394-turnstile.js +++ b/packages/backend/migration/1664694635394-turnstile.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1665091090561-add-renote-muting.js b/packages/backend/migration/1665091090561-add-renote-muting.js index 5748572517..4774fec987 100644 --- a/packages/backend/migration/1665091090561-add-renote-muting.js +++ b/packages/backend/migration/1665091090561-add-renote-muting.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js index 431241897d..e1b7d2a02f 100644 --- a/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js +++ b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1671924750884-RetentionAggregation.js b/packages/backend/migration/1671924750884-RetentionAggregation.js index 67079bb7a1..956d780b46 100644 --- a/packages/backend/migration/1671924750884-RetentionAggregation.js +++ b/packages/backend/migration/1671924750884-RetentionAggregation.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1671926422832-RetentionAggregation2.js b/packages/backend/migration/1671926422832-RetentionAggregation2.js index f26e0f7d2e..357c6b08e1 100644 --- a/packages/backend/migration/1671926422832-RetentionAggregation2.js +++ b/packages/backend/migration/1671926422832-RetentionAggregation2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1672562400597-PerUserPvChart.js b/packages/backend/migration/1672562400597-PerUserPvChart.js index 844f665a8b..e627f571ef 100644 --- a/packages/backend/migration/1672562400597-PerUserPvChart.js +++ b/packages/backend/migration/1672562400597-PerUserPvChart.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js b/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js index fa73fc8977..613826116a 100644 --- a/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js +++ b/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js index abf209162b..9ff6a59431 100644 --- a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js +++ b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1672704136584-remove-latestStatus.js b/packages/backend/migration/1672704136584-remove-latestStatus.js index d75344c053..96d16e770f 100644 --- a/packages/backend/migration/1672704136584-remove-latestStatus.js +++ b/packages/backend/migration/1672704136584-remove-latestStatus.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1672822262496-Flash.js b/packages/backend/migration/1672822262496-Flash.js index fd3f77d893..70384d6f14 100644 --- a/packages/backend/migration/1672822262496-Flash.js +++ b/packages/backend/migration/1672822262496-Flash.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673336077243-PollChoiceLength.js b/packages/backend/migration/1673336077243-PollChoiceLength.js index 7bd65149d6..9eefdcc7ee 100644 --- a/packages/backend/migration/1673336077243-PollChoiceLength.js +++ b/packages/backend/migration/1673336077243-PollChoiceLength.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673500412259-Role.js b/packages/backend/migration/1673500412259-Role.js index 6bfb31e08e..e3dd058132 100644 --- a/packages/backend/migration/1673500412259-Role.js +++ b/packages/backend/migration/1673500412259-Role.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673515526953-RoleColor.js b/packages/backend/migration/1673515526953-RoleColor.js index b856e4183b..5cd4b4d0d0 100644 --- a/packages/backend/migration/1673515526953-RoleColor.js +++ b/packages/backend/migration/1673515526953-RoleColor.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673522856499-RoleIroiro.js b/packages/backend/migration/1673522856499-RoleIroiro.js index 40635e50d8..79d58e4577 100644 --- a/packages/backend/migration/1673522856499-RoleIroiro.js +++ b/packages/backend/migration/1673522856499-RoleIroiro.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673524604156-RoleLastUsedAt.js b/packages/backend/migration/1673524604156-RoleLastUsedAt.js index 3bbb8000d8..aed43c696f 100644 --- a/packages/backend/migration/1673524604156-RoleLastUsedAt.js +++ b/packages/backend/migration/1673524604156-RoleLastUsedAt.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673570377815-RoleConditional.js b/packages/backend/migration/1673570377815-RoleConditional.js index 354fd6c66a..6cb2450912 100644 --- a/packages/backend/migration/1673570377815-RoleConditional.js +++ b/packages/backend/migration/1673570377815-RoleConditional.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673575973645-MetaClean.js b/packages/backend/migration/1673575973645-MetaClean.js index 684d62e8e9..422b83a5ff 100644 --- a/packages/backend/migration/1673575973645-MetaClean.js +++ b/packages/backend/migration/1673575973645-MetaClean.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673783015567-Policies.js b/packages/backend/migration/1673783015567-Policies.js index 8674306620..181875faf4 100644 --- a/packages/backend/migration/1673783015567-Policies.js +++ b/packages/backend/migration/1673783015567-Policies.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1673812883772-firstRetrievedAt.js b/packages/backend/migration/1673812883772-firstRetrievedAt.js index 4111cc4ad0..6b472627ab 100644 --- a/packages/backend/migration/1673812883772-firstRetrievedAt.js +++ b/packages/backend/migration/1673812883772-firstRetrievedAt.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1674086433654-flashScriptLength.js b/packages/backend/migration/1674086433654-flashScriptLength.js index cdfb812ba0..f49493a44e 100644 --- a/packages/backend/migration/1674086433654-flashScriptLength.js +++ b/packages/backend/migration/1674086433654-flashScriptLength.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1674118260469-achievement.js b/packages/backend/migration/1674118260469-achievement.js index 072cf81ec3..14c651f221 100644 --- a/packages/backend/migration/1674118260469-achievement.js +++ b/packages/backend/migration/1674118260469-achievement.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1674255666603-loggedInDates.js b/packages/backend/migration/1674255666603-loggedInDates.js index a2a217da95..b4d72dea22 100644 --- a/packages/backend/migration/1674255666603-loggedInDates.js +++ b/packages/backend/migration/1674255666603-loggedInDates.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1675053125067-fixforeignkeyreports.js b/packages/backend/migration/1675053125067-fixforeignkeyreports.js index 2ca383f563..6ec2a08f3a 100644 --- a/packages/backend/migration/1675053125067-fixforeignkeyreports.js +++ b/packages/backend/migration/1675053125067-fixforeignkeyreports.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1675404035646-cleanup.js b/packages/backend/migration/1675404035646-cleanup.js index 5cd5f5534a..f0945b412d 100644 --- a/packages/backend/migration/1675404035646-cleanup.js +++ b/packages/backend/migration/1675404035646-cleanup.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1675557528704-role-icon-badge.js b/packages/backend/migration/1675557528704-role-icon-badge.js index 48684075d1..5b6d6cd035 100644 --- a/packages/backend/migration/1675557528704-role-icon-badge.js +++ b/packages/backend/migration/1675557528704-role-icon-badge.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1676438468213-ad3.js b/packages/backend/migration/1676438468213-ad3.js index 83ca5828e3..6acdae1894 100644 --- a/packages/backend/migration/1676438468213-ad3.js +++ b/packages/backend/migration/1676438468213-ad3.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1677054292210-ad4.js b/packages/backend/migration/1677054292210-ad4.js index 11c42dd354..5478eef96b 100644 --- a/packages/backend/migration/1677054292210-ad4.js +++ b/packages/backend/migration/1677054292210-ad4.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1677570181236-role-assignment-expires-at.js b/packages/backend/migration/1677570181236-role-assignment-expires-at.js index 6fe32ffeb0..f0cfae8e44 100644 --- a/packages/backend/migration/1677570181236-role-assignment-expires-at.js +++ b/packages/backend/migration/1677570181236-role-assignment-expires-at.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js b/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js index 44c807499c..2fe749a268 100644 --- a/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js +++ b/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1678426061773-tweak-varchar-length.js b/packages/backend/migration/1678426061773-tweak-varchar-length.js index 74c4fd6715..fa28fa6e01 100644 --- a/packages/backend/migration/1678426061773-tweak-varchar-length.js +++ b/packages/backend/migration/1678426061773-tweak-varchar-length.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1678427401214-remove-unused.js b/packages/backend/migration/1678427401214-remove-unused.js index e398b3700c..319ad8dd50 100644 --- a/packages/backend/migration/1678427401214-remove-unused.js +++ b/packages/backend/migration/1678427401214-remove-unused.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1678602320354-role-display-order.js b/packages/backend/migration/1678602320354-role-display-order.js index d3cc9792ca..246a4f67ff 100644 --- a/packages/backend/migration/1678602320354-role-display-order.js +++ b/packages/backend/migration/1678602320354-role-display-order.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1678694614599-sensitive-words.js b/packages/backend/migration/1678694614599-sensitive-words.js index 13361f597e..841778d7c5 100644 --- a/packages/backend/migration/1678694614599-sensitive-words.js +++ b/packages/backend/migration/1678694614599-sensitive-words.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1678869617549-retention-date-key.js b/packages/backend/migration/1678869617549-retention-date-key.js index 1b995385b0..94e7bc26a1 100644 --- a/packages/backend/migration/1678869617549-retention-date-key.js +++ b/packages/backend/migration/1678869617549-retention-date-key.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js index 5d1218be12..8486d5501f 100644 --- a/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js +++ b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1678953978856-clip-favorite.js b/packages/backend/migration/1678953978856-clip-favorite.js index 9d706c4dae..12204ca4fb 100644 --- a/packages/backend/migration/1678953978856-clip-favorite.js +++ b/packages/backend/migration/1678953978856-clip-favorite.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1679309757174-antenna-active.js b/packages/backend/migration/1679309757174-antenna-active.js index dadea25a7c..a252981c93 100644 --- a/packages/backend/migration/1679309757174-antenna-active.js +++ b/packages/backend/migration/1679309757174-antenna-active.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js index f2a13100e2..1a41d2842b 100644 --- a/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js +++ b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1679651580149-cleanup.js b/packages/backend/migration/1679651580149-cleanup.js index efee339c46..dc58141f34 100644 --- a/packages/backend/migration/1679651580149-cleanup.js +++ b/packages/backend/migration/1679651580149-cleanup.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js index 67be10e6fd..e34a97daa4 100644 --- a/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js +++ b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1680228513388-channelFavorite.js b/packages/backend/migration/1680228513388-channelFavorite.js index 866173305e..80752d37d2 100644 --- a/packages/backend/migration/1680228513388-channelFavorite.js +++ b/packages/backend/migration/1680228513388-channelFavorite.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1680238118084-channelNotePining.js b/packages/backend/migration/1680238118084-channelNotePining.js index 78bafc0237..796aa80f5d 100644 --- a/packages/backend/migration/1680238118084-channelNotePining.js +++ b/packages/backend/migration/1680238118084-channelNotePining.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1680491187535-cleanup.js b/packages/backend/migration/1680491187535-cleanup.js index f0b1bccdab..4596503323 100644 --- a/packages/backend/migration/1680491187535-cleanup.js +++ b/packages/backend/migration/1680491187535-cleanup.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1680582195041-cleanup.js b/packages/backend/migration/1680582195041-cleanup.js index 83d04b6186..5b84c03b36 100644 --- a/packages/backend/migration/1680582195041-cleanup.js +++ b/packages/backend/migration/1680582195041-cleanup.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1680702787050-UserMemo.js b/packages/backend/migration/1680702787050-UserMemo.js index 3f7afe8657..c408cf4755 100644 --- a/packages/backend/migration/1680702787050-UserMemo.js +++ b/packages/backend/migration/1680702787050-UserMemo.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js b/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js index 49295e70eb..6c71c4bc62 100644 --- a/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js +++ b/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1680931179228-account-move.js b/packages/backend/migration/1680931179228-account-move.js index a8b5e4df68..de6ea972b0 100644 --- a/packages/backend/migration/1680931179228-account-move.js +++ b/packages/backend/migration/1680931179228-account-move.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1681400427971-serverRules.js b/packages/backend/migration/1681400427971-serverRules.js index 176783b50a..ae62577799 100644 --- a/packages/backend/migration/1681400427971-serverRules.js +++ b/packages/backend/migration/1681400427971-serverRules.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1681429921400-Event.js b/packages/backend/migration/1681429921400-Event.js index ff6b68f16d..2cd2657a12 100644 --- a/packages/backend/migration/1681429921400-Event.js +++ b/packages/backend/migration/1681429921400-Event.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1681673280586-event.js b/packages/backend/migration/1681673280586-event.js index d7bb78436e..58cb9f6c8a 100644 --- a/packages/backend/migration/1681673280586-event.js +++ b/packages/backend/migration/1681673280586-event.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1681675881633-event.js b/packages/backend/migration/1681675881633-event.js index f61707d618..4869ab3f77 100644 --- a/packages/backend/migration/1681675881633-event.js +++ b/packages/backend/migration/1681675881633-event.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1681870960239-RoleTLSetting.js b/packages/backend/migration/1681870960239-RoleTLSetting.js index 2999051a3b..2a95b39dc8 100644 --- a/packages/backend/migration/1681870960239-RoleTLSetting.js +++ b/packages/backend/migration/1681870960239-RoleTLSetting.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1682190963894-movedAt.js b/packages/backend/migration/1682190963894-movedAt.js index 852cf58969..f7be25019f 100644 --- a/packages/backend/migration/1682190963894-movedAt.js +++ b/packages/backend/migration/1682190963894-movedAt.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1682754135458-preservedUsernames.js b/packages/backend/migration/1682754135458-preservedUsernames.js index e441e732cd..6d128affdf 100644 --- a/packages/backend/migration/1682754135458-preservedUsernames.js +++ b/packages/backend/migration/1682754135458-preservedUsernames.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1682985520254-channelColor.js b/packages/backend/migration/1682985520254-channelColor.js index 3c7f3101a5..8a1c18cbdc 100644 --- a/packages/backend/migration/1682985520254-channelColor.js +++ b/packages/backend/migration/1682985520254-channelColor.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1683328299359-channelArchive.js b/packages/backend/migration/1683328299359-channelArchive.js index 10a87246de..3b38ae384e 100644 --- a/packages/backend/migration/1683328299359-channelArchive.js +++ b/packages/backend/migration/1683328299359-channelArchive.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1683682889948-prevent-ai-larning.js b/packages/backend/migration/1683682889948-prevent-ai-larning.js index 167c9f71d2..2b03b4176d 100644 --- a/packages/backend/migration/1683682889948-prevent-ai-larning.js +++ b/packages/backend/migration/1683682889948-prevent-ai-larning.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1683683083083-public-reactions-default-true.js b/packages/backend/migration/1683683083083-public-reactions-default-true.js index f416e5ffa7..e259a3f936 100644 --- a/packages/backend/migration/1683683083083-public-reactions-default-true.js +++ b/packages/backend/migration/1683683083083-public-reactions-default-true.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1683789676867-fix-typo.js b/packages/backend/migration/1683789676867-fix-typo.js index d647d20e62..e6a4a19397 100644 --- a/packages/backend/migration/1683789676867-fix-typo.js +++ b/packages/backend/migration/1683789676867-fix-typo.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1683847157541-UserList.js b/packages/backend/migration/1683847157541-UserList.js index 14a52d64f8..1602d3e318 100644 --- a/packages/backend/migration/1683847157541-UserList.js +++ b/packages/backend/migration/1683847157541-UserList.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1683869758873-UserListFavorites.js b/packages/backend/migration/1683869758873-UserListFavorites.js index aae4056845..ebeb29e9af 100644 --- a/packages/backend/migration/1683869758873-UserListFavorites.js +++ b/packages/backend/migration/1683869758873-UserListFavorites.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js index 398f9f0803..a50b4caa96 100644 --- a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js +++ b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1684231006000-tweak-varchar-length2.js b/packages/backend/migration/1684231006000-tweak-varchar-length2.js index 1935b3293b..af58cf578a 100644 --- a/packages/backend/migration/1684231006000-tweak-varchar-length2.js +++ b/packages/backend/migration/1684231006000-tweak-varchar-length2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1684386446061-emoji-improve.js b/packages/backend/migration/1684386446061-emoji-improve.js index e7e94769b8..0803a28369 100644 --- a/packages/backend/migration/1684386446061-emoji-improve.js +++ b/packages/backend/migration/1684386446061-emoji-improve.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1685378242713-multipleTranslationServices.js b/packages/backend/migration/1685378242713-multipleTranslationServices.js index 2e839e08a3..ea97170a8f 100644 --- a/packages/backend/migration/1685378242713-multipleTranslationServices.js +++ b/packages/backend/migration/1685378242713-multipleTranslationServices.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1685973839966-errorImageUrl.js b/packages/backend/migration/1685973839966-errorImageUrl.js index ca685ef088..33597d78d5 100644 --- a/packages/backend/migration/1685973839966-errorImageUrl.js +++ b/packages/backend/migration/1685973839966-errorImageUrl.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1686908762393-AbuseReportResolver.js b/packages/backend/migration/1686908762393-AbuseReportResolver.js index 94698736d3..e8138b27d7 100644 --- a/packages/backend/migration/1686908762393-AbuseReportResolver.js +++ b/packages/backend/migration/1686908762393-AbuseReportResolver.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1688280713783-add-meta-options.js b/packages/backend/migration/1688280713783-add-meta-options.js index 77d1934925..803b9bc5b2 100644 --- a/packages/backend/migration/1688280713783-add-meta-options.js +++ b/packages/backend/migration/1688280713783-add-meta-options.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1688720440658-refactor-invite-system.js b/packages/backend/migration/1688720440658-refactor-invite-system.js index ea192a1950..972b620298 100644 --- a/packages/backend/migration/1688720440658-refactor-invite-system.js +++ b/packages/backend/migration/1688720440658-refactor-invite-system.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1688880985544-add-index-to-relations.js b/packages/backend/migration/1688880985544-add-index-to-relations.js index c18903641c..5930d24ebe 100644 --- a/packages/backend/migration/1688880985544-add-index-to-relations.js +++ b/packages/backend/migration/1688880985544-add-index-to-relations.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1689102832143-nsfw-cache.js b/packages/backend/migration/1689102832143-nsfw-cache.js index 90d453418b..9f46a65037 100644 --- a/packages/backend/migration/1689102832143-nsfw-cache.js +++ b/packages/backend/migration/1689102832143-nsfw-cache.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js index 2dc7774493..ce246b20f8 100644 --- a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js +++ b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class UserBlacklistAnntena1689325027964 { name = 'UserBlacklistAnntena1689325027964' diff --git a/packages/backend/migration/1689580926821-ObjectStorageRemoteSetting.js b/packages/backend/migration/1689580926821-ObjectStorageRemoteSetting.js index db3ae7be3c..d0307bae7a 100644 --- a/packages/backend/migration/1689580926821-ObjectStorageRemoteSetting.js +++ b/packages/backend/migration/1689580926821-ObjectStorageRemoteSetting.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1690417561185-fix-renote-muting.js b/packages/backend/migration/1690417561185-fix-renote-muting.js index d9604ca26c..14150b0362 100644 --- a/packages/backend/migration/1690417561185-fix-renote-muting.js +++ b/packages/backend/migration/1690417561185-fix-renote-muting.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class FixRenoteMuting1690417561185 { name = 'FixRenoteMuting1690417561185' diff --git a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js index 9bccdb3bb5..7eda5debe5 100644 --- a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js +++ b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ChangeCacheRemoteFilesDefault1690417561186 { name = 'ChangeCacheRemoteFilesDefault1690417561186' diff --git a/packages/backend/migration/1690417561187-Fix.js b/packages/backend/migration/1690417561187-Fix.js index 6275662c23..e48e069f40 100644 --- a/packages/backend/migration/1690417561187-Fix.js +++ b/packages/backend/migration/1690417561187-Fix.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class Fix1690417561187 { name = 'Fix1690417561187' diff --git a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js index a3ef8dcf08..2049df8ea2 100644 --- a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js +++ b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class User2faBackupCodes1690569881926 { name = 'User2faBackupCodes1690569881926' diff --git a/packages/backend/migration/1690782653311-SensitiveChannel.js b/packages/backend/migration/1690782653311-SensitiveChannel.js index afec1a2153..921281036f 100644 --- a/packages/backend/migration/1690782653311-SensitiveChannel.js +++ b/packages/backend/migration/1690782653311-SensitiveChannel.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1690796169261-play-visibility.js b/packages/backend/migration/1690796169261-play-visibility.js index 5e5843bfee..cb21f25b1f 100644 --- a/packages/backend/migration/1690796169261-play-visibility.js +++ b/packages/backend/migration/1690796169261-play-visibility.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1691120548582-notification-emails-for-abuse-report.js b/packages/backend/migration/1691120548582-notification-emails-for-abuse-report.js index af9018267a..6837c7fab7 100644 --- a/packages/backend/migration/1691120548582-notification-emails-for-abuse-report.js +++ b/packages/backend/migration/1691120548582-notification-emails-for-abuse-report.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1691649257651-refine-announcement.js b/packages/backend/migration/1691649257651-refine-announcement.js index ac621155d5..d8d63f3103 100644 --- a/packages/backend/migration/1691649257651-refine-announcement.js +++ b/packages/backend/migration/1691649257651-refine-announcement.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RefineAnnouncement1691649257651 { name = 'RefineAnnouncement1691649257651' diff --git a/packages/backend/migration/1691657412740-refine-announcement-2.js b/packages/backend/migration/1691657412740-refine-announcement-2.js index 67edf19659..8791f99f44 100644 --- a/packages/backend/migration/1691657412740-refine-announcement-2.js +++ b/packages/backend/migration/1691657412740-refine-announcement-2.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RefineAnnouncement21691657412740 { name = 'RefineAnnouncement21691657412740' diff --git a/packages/backend/migration/1691959191872-passkey-support.js b/packages/backend/migration/1691959191872-passkey-support.js index 1da9bdb363..455c78dc1e 100644 --- a/packages/backend/migration/1691959191872-passkey-support.js +++ b/packages/backend/migration/1691959191872-passkey-support.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1694850832075-server-icons-and-manifest.js b/packages/backend/migration/1694850832075-server-icons-and-manifest.js index 235bf05744..881a8521d8 100644 --- a/packages/backend/migration/1694850832075-server-icons-and-manifest.js +++ b/packages/backend/migration/1694850832075-server-icons-and-manifest.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1694915420864-clipped-count.js b/packages/backend/migration/1694915420864-clipped-count.js index 6d70aaecf1..7e8adae227 100644 --- a/packages/backend/migration/1694915420864-clipped-count.js +++ b/packages/backend/migration/1694915420864-clipped-count.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1695260774117-verified-links.js b/packages/backend/migration/1695260774117-verified-links.js index 64c8a9ad8f..18e0571d81 100644 --- a/packages/backend/migration/1695260774117-verified-links.js +++ b/packages/backend/migration/1695260774117-verified-links.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class VerifiedLinks1695260774117 { name = 'VerifiedLinks1695260774117' diff --git a/packages/backend/migration/1695288787870-following-notify.js b/packages/backend/migration/1695288787870-following-notify.js index b3f78d5f2a..e7e2194b15 100644 --- a/packages/backend/migration/1695288787870-following-notify.js +++ b/packages/backend/migration/1695288787870-following-notify.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class FollowingNotify1695288787870 { name = 'FollowingNotify1695288787870' diff --git a/packages/backend/migration/1695440131671-short-name.js b/packages/backend/migration/1695440131671-short-name.js index fdc256caf8..2c37297fc1 100644 --- a/packages/backend/migration/1695440131671-short-name.js +++ b/packages/backend/migration/1695440131671-short-name.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class ShortName1695440131671 { name = 'ShortName1695440131671' diff --git a/packages/backend/migration/1695605508898-mutingNotificationTypes.js b/packages/backend/migration/1695605508898-mutingNotificationTypes.js index 67d4243142..8c0e52a2f0 100644 --- a/packages/backend/migration/1695605508898-mutingNotificationTypes.js +++ b/packages/backend/migration/1695605508898-mutingNotificationTypes.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class MutingNotificationTypes1695605508898 { name = 'MutingNotificationTypes1695605508898' diff --git a/packages/backend/migration/1695901659683-note-updated-at.js b/packages/backend/migration/1695901659683-note-updated-at.js index e828fb1a6f..d8a151a1f7 100644 --- a/packages/backend/migration/1695901659683-note-updated-at.js +++ b/packages/backend/migration/1695901659683-note-updated-at.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class NoteUpdatedAt1695901659683 { name = 'NoteUpdatedAt1695901659683' diff --git a/packages/backend/migration/1695944637565-notificationRecieveConfig.js b/packages/backend/migration/1695944637565-notificationRecieveConfig.js index 04a40993c0..4c75db28ff 100644 --- a/packages/backend/migration/1695944637565-notificationRecieveConfig.js +++ b/packages/backend/migration/1695944637565-notificationRecieveConfig.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696003580220-AddSomeUrls.js b/packages/backend/migration/1696003580220-AddSomeUrls.js index 213e39e7af..660cc2b099 100644 --- a/packages/backend/migration/1696003580220-AddSomeUrls.js +++ b/packages/backend/migration/1696003580220-AddSomeUrls.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696044626209-noteEditHistory.js b/packages/backend/migration/1696044626209-noteEditHistory.js index 2c2c80886a..acfc4608a2 100644 --- a/packages/backend/migration/1696044626209-noteEditHistory.js +++ b/packages/backend/migration/1696044626209-noteEditHistory.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696222183852-withReplies.js b/packages/backend/migration/1696222183852-withReplies.js index 84a5511d17..18429c0d69 100644 --- a/packages/backend/migration/1696222183852-withReplies.js +++ b/packages/backend/migration/1696222183852-withReplies.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696318192428-noteUpdatedAtHistory.js b/packages/backend/migration/1696318192428-noteUpdatedAtHistory.js index df82342416..70220f3058 100644 --- a/packages/backend/migration/1696318192428-noteUpdatedAtHistory.js +++ b/packages/backend/migration/1696318192428-noteUpdatedAtHistory.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696323464251-user-list-membership.js b/packages/backend/migration/1696323464251-user-list-membership.js index dc1d438dd7..7534040c4c 100644 --- a/packages/backend/migration/1696323464251-user-list-membership.js +++ b/packages/backend/migration/1696323464251-user-list-membership.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class UserListMembership1696323464251 { name = 'UserListMembership1696323464251' diff --git a/packages/backend/migration/1696331570827-hibernation.js b/packages/backend/migration/1696331570827-hibernation.js index 1487ece77c..119d35913f 100644 --- a/packages/backend/migration/1696331570827-hibernation.js +++ b/packages/backend/migration/1696331570827-hibernation.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class Hibernation1696331570827 { name = 'Hibernation1696331570827' diff --git a/packages/backend/migration/1696332072038-clean.js b/packages/backend/migration/1696332072038-clean.js index 23dca55fdc..45b7511acc 100644 --- a/packages/backend/migration/1696332072038-clean.js +++ b/packages/backend/migration/1696332072038-clean.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class Clean1696332072038 { name = 'Clean1696332072038' diff --git a/packages/backend/migration/1696373953614-meta-cache-settings.js b/packages/backend/migration/1696373953614-meta-cache-settings.js index cbbe471d45..a99afe7dce 100644 --- a/packages/backend/migration/1696373953614-meta-cache-settings.js +++ b/packages/backend/migration/1696373953614-meta-cache-settings.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696402675000-add-meta-options.js b/packages/backend/migration/1696402675000-add-meta-options.js index 2dc55ef7db..9941edc6ae 100644 --- a/packages/backend/migration/1696402675000-add-meta-options.js +++ b/packages/backend/migration/1696402675000-add-meta-options.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696405744672-clean-up.js b/packages/backend/migration/1696405744672-clean-up.js index 4e1ee6cd61..e8a742a2d1 100644 --- a/packages/backend/migration/1696405744672-clean-up.js +++ b/packages/backend/migration/1696405744672-clean-up.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696417300000-add-meta-options.js b/packages/backend/migration/1696417300000-add-meta-options.js index c2660dd1b0..d9b13e2179 100644 --- a/packages/backend/migration/1696417300000-add-meta-options.js +++ b/packages/backend/migration/1696417300000-add-meta-options.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696569742153-clean-up.js b/packages/backend/migration/1696569742153-clean-up.js index b7c981bab2..661d6aeaca 100644 --- a/packages/backend/migration/1696569742153-clean-up.js +++ b/packages/backend/migration/1696569742153-clean-up.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696581429196-clean-up.js b/packages/backend/migration/1696581429196-clean-up.js index b6723f3430..d75d37f726 100644 --- a/packages/backend/migration/1696581429196-clean-up.js +++ b/packages/backend/migration/1696581429196-clean-up.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696604572677-poll-vote-poll.js b/packages/backend/migration/1696604572677-poll-vote-poll.js index 32c2e867fb..da52904565 100644 --- a/packages/backend/migration/1696604572677-poll-vote-poll.js +++ b/packages/backend/migration/1696604572677-poll-vote-poll.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class PollVotePoll1696604572677 { name = 'PollVotePoll1696604572677'; diff --git a/packages/backend/migration/1696743032098-AdsOnStream.js b/packages/backend/migration/1696743032098-AdsOnStream.js index 43b9f83e66..d0bf699bc4 100644 --- a/packages/backend/migration/1696743032098-AdsOnStream.js +++ b/packages/backend/migration/1696743032098-AdsOnStream.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696807733453-userListUserId.js b/packages/backend/migration/1696807733453-userListUserId.js index b57350175e..253352ccb5 100644 --- a/packages/backend/migration/1696807733453-userListUserId.js +++ b/packages/backend/migration/1696807733453-userListUserId.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1696808725134-userListUserId-2.js b/packages/backend/migration/1696808725134-userListUserId-2.js index cc504e761c..ded6b22b12 100644 --- a/packages/backend/migration/1696808725134-userListUserId-2.js +++ b/packages/backend/migration/1696808725134-userListUserId-2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1697247230117-InstanceSilence.js b/packages/backend/migration/1697247230117-InstanceSilence.js index 309d817087..e257550d0d 100644 --- a/packages/backend/migration/1697247230117-InstanceSilence.js +++ b/packages/backend/migration/1697247230117-InstanceSilence.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1697420555911-deleteCreatedAt.js b/packages/backend/migration/1697420555911-deleteCreatedAt.js index 43c9f8ffc5..ea423507ad 100644 --- a/packages/backend/migration/1697420555911-deleteCreatedAt.js +++ b/packages/backend/migration/1697420555911-deleteCreatedAt.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1697436246389-antenna-localOnly.js b/packages/backend/migration/1697436246389-antenna-localOnly.js index d7c0ca6510..15d147f210 100644 --- a/packages/backend/migration/1697436246389-antenna-localOnly.js +++ b/packages/backend/migration/1697436246389-antenna-localOnly.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1697441463087-FollowRequestWithReplies.js b/packages/backend/migration/1697441463087-FollowRequestWithReplies.js index 58b61aff63..889200bc95 100644 --- a/packages/backend/migration/1697441463087-FollowRequestWithReplies.js +++ b/packages/backend/migration/1697441463087-FollowRequestWithReplies.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js b/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js index fab07fd3f4..4404e03752 100644 --- a/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js +++ b/packages/backend/migration/1697673894459-note-reactionAndUserPairCache.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1697737204579-deleteCreatedAt.js b/packages/backend/migration/1697737204579-deleteCreatedAt.js index 6ac568c681..8f801e7913 100644 --- a/packages/backend/migration/1697737204579-deleteCreatedAt.js +++ b/packages/backend/migration/1697737204579-deleteCreatedAt.js @@ -1,7 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project - * SPDX-License-Identifier: AGPL-3.0-only - */ export class DeleteCreatedAt1697737204579 { name = 'DeleteCreatedAt1697737204579' diff --git a/packages/backend/migration/1697847397844-avatar-decoration.js b/packages/backend/migration/1697847397844-avatar-decoration.js index 32ee47e968..c8427feaa5 100644 --- a/packages/backend/migration/1697847397844-avatar-decoration.js +++ b/packages/backend/migration/1697847397844-avatar-decoration.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1697941908548-avatar-decoration2.js b/packages/backend/migration/1697941908548-avatar-decoration2.js index 58344e2bb6..7e2d16b356 100644 --- a/packages/backend/migration/1697941908548-avatar-decoration2.js +++ b/packages/backend/migration/1697941908548-avatar-decoration2.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1698041201306-enable-ftt.js b/packages/backend/migration/1698041201306-enable-ftt.js index c67dda6f5f..4c0f528885 100644 --- a/packages/backend/migration/1698041201306-enable-ftt.js +++ b/packages/backend/migration/1698041201306-enable-ftt.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1698840138000-add-allow-renote-to-external.js b/packages/backend/migration/1698840138000-add-allow-renote-to-external.js index 8ce35b0f69..8f017c1068 100644 --- a/packages/backend/migration/1698840138000-add-allow-renote-to-external.js +++ b/packages/backend/migration/1698840138000-add-allow-renote-to-external.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1699141698112-announcement-silence.js b/packages/backend/migration/1699141698112-announcement-silence.js index f462d30b51..f3e56e4547 100644 --- a/packages/backend/migration/1699141698112-announcement-silence.js +++ b/packages/backend/migration/1699141698112-announcement-silence.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1699432324194-remoteAvaterDecoration.js b/packages/backend/migration/1699432324194-remoteAvaterDecoration.js index c5e0dd53af..5b2762b476 100644 --- a/packages/backend/migration/1699432324194-remoteAvaterDecoration.js +++ b/packages/backend/migration/1699432324194-remoteAvaterDecoration.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class RemoteAvaterDecoration1699432324194 { name = 'RemoteAvaterDecoration1699432324194' diff --git a/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js b/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js index 2ab93624ce..01745e9f05 100644 --- a/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js +++ b/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1700303245007-supportVerifyMailApi.js b/packages/backend/migration/1700303245007-supportVerifyMailApi.js index 58ff7a69c4..8d06372779 100644 --- a/packages/backend/migration/1700303245007-supportVerifyMailApi.js +++ b/packages/backend/migration/1700303245007-supportVerifyMailApi.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js index 92c3ada4a1..afd3247f5c 100644 --- a/packages/backend/migration/1700383825690-hard-mute.js +++ b/packages/backend/migration/1700383825690-hard-mute.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class HardMute1700383825690 { name = 'HardMute1700383825690' diff --git a/packages/backend/migration/1700902349231-add-bday-index.js b/packages/backend/migration/1700902349231-add-bday-index.js index c58165c70e..71e3e1233e 100644 --- a/packages/backend/migration/1700902349231-add-bday-index.js +++ b/packages/backend/migration/1700902349231-add-bday-index.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1702718871541-ffVisibility.js b/packages/backend/migration/1702718871541-ffVisibility.js index 164af00f25..8908a496db 100644 --- a/packages/backend/migration/1702718871541-ffVisibility.js +++ b/packages/backend/migration/1702718871541-ffVisibility.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1703209889304-bannedEmailDomains.js b/packages/backend/migration/1703209889304-bannedEmailDomains.js index 2fdd4e1183..8d7f09e22e 100644 --- a/packages/backend/migration/1703209889304-bannedEmailDomains.js +++ b/packages/backend/migration/1703209889304-bannedEmailDomains.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/migration/1703658526000-supportTrueMailApi.js b/packages/backend/migration/1703658526000-supportTrueMailApi.js deleted file mode 100644 index fb62653e40..0000000000 --- a/packages/backend/migration/1703658526000-supportTrueMailApi.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SupportTrueMailApi1703658526000 { - name = 'SupportTrueMailApi1703658526000' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "truemailInstance" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "truemailAuthKey" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableTruemailApi" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTruemailApi"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailInstance"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailAuthKey"`); - } -} diff --git a/packages/backend/migration/1704185628000-note-updated-at2.js b/packages/backend/migration/1704185628000-note-updated-at2.js index 1eca703214..b3fee061e4 100644 --- a/packages/backend/migration/1704185628000-note-updated-at2.js +++ b/packages/backend/migration/1704185628000-note-updated-at2.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export class NoteUpdatedAt1704185628000 { name = 'NoteUpdatedAt1704185628000' diff --git a/packages/backend/migration/1704373210054-support-mcaptcha.js b/packages/backend/migration/1704373210054-support-mcaptcha.js deleted file mode 100644 index 50b4801e14..0000000000 --- a/packages/backend/migration/1704373210054-support-mcaptcha.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SupportMcaptcha1704373210054 { - name = 'SupportMcaptcha1704373210054' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableMcaptcha" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSitekey" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSecretKey" character varying(1024)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaInstanceUrl" character varying(1024)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaInstanceUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSecretKey"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSitekey"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableMcaptcha"`); - } -} diff --git a/packages/backend/migration/1704959805077-bubble-game-record.js b/packages/backend/migration/1704959805077-bubble-game-record.js deleted file mode 100644 index 6c4d7ab1a9..0000000000 --- a/packages/backend/migration/1704959805077-bubble-game-record.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class BubbleGameRecord1704959805077 { - name = 'BubbleGameRecord1704959805077' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "bubble_game_record" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "seededAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seed" character varying(1024) NOT NULL, "gameVersion" integer NOT NULL, "gameMode" character varying(128) NOT NULL, "score" integer NOT NULL, "logs" jsonb NOT NULL DEFAULT '[]', "isVerified" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a75395fe404b392e2893b50d7ea" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_75276757070d21fdfaf4c05290" ON "bubble_game_record" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_4ae7053179014915d1432d3f40" ON "bubble_game_record" ("seededAt") `); - await queryRunner.query(`CREATE INDEX "IDX_26d4ee490b5a487142d35466ee" ON "bubble_game_record" ("score") `); - await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD CONSTRAINT "FK_75276757070d21fdfaf4c052909" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "bubble_game_record" DROP CONSTRAINT "FK_75276757070d21fdfaf4c052909"`); - await queryRunner.query(`DROP INDEX "public"."IDX_26d4ee490b5a487142d35466ee"`); - await queryRunner.query(`DROP INDEX "public"."IDX_4ae7053179014915d1432d3f40"`); - await queryRunner.query(`DROP INDEX "public"."IDX_75276757070d21fdfaf4c05290"`); - await queryRunner.query(`DROP TABLE "bubble_game_record"`); - } -} diff --git a/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js b/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js deleted file mode 100644 index fe0a5a2bcf..0000000000 --- a/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class OptimizeNoteIndexForArrayColumns1705222772858 { - name = 'OptimizeNoteIndexForArrayColumns1705222772858' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_796a8c03959361f97dc2be1d5c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_54ebcb6d27222913b908d56fd8"`); - await queryRunner.query(`DROP INDEX "public"."IDX_88937d94d7443d9a99a76fa5c0"`); - await queryRunner.query(`DROP INDEX "public"."IDX_51c063b6a133a9cb87145450f5"`); - await queryRunner.query(`CREATE INDEX "IDX_NOTE_FILE_IDS" ON "note" using gin ("fileIds")`) - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_NOTE_FILE_IDS"`) - await queryRunner.query(`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds") `); - await queryRunner.query(`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags") `); - await queryRunner.query(`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions") `); - await queryRunner.query(`CREATE INDEX "IDX_796a8c03959361f97dc2be1d5c" ON "note" ("visibleUserIds") `); - } -} diff --git a/packages/backend/migration/1705475608437-reversi.js b/packages/backend/migration/1705475608437-reversi.js deleted file mode 100644 index 9921728457..0000000000 --- a/packages/backend/migration/1705475608437-reversi.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi1705475608437 { - name = 'Reversi1705475608437' - - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_b46ec40746efceac604142be1c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b604d92d6c7aec38627f6eaf16"`); - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "createdAt"`); - await queryRunner.query(`ALTER TABLE "reversi_matching" DROP COLUMN "createdAt"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_matching" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt") `); - } -} diff --git a/packages/backend/migration/1705654039457-reversi-2.js b/packages/backend/migration/1705654039457-reversi-2.js deleted file mode 100644 index 6685dca73b..0000000000 --- a/packages/backend/migration/1705654039457-reversi-2.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi21705654039457 { - name = 'Reversi21705654039457' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Accepted" TO "user1Ready"`); - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Accepted" TO "user2Ready"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Ready" TO "user1Accepted"`); - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Ready" TO "user2Accepted"`); - } -} diff --git a/packages/backend/migration/1705793785675-reversi-3.js b/packages/backend/migration/1705793785675-reversi-3.js deleted file mode 100644 index 94b1e4fac9..0000000000 --- a/packages/backend/migration/1705793785675-reversi-3.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi31705793785675 { - name = 'Reversi31705793785675' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "surrendered" TO "surrenderedUserId"`); - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "timeoutUserId" character varying(32)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "timeoutUserId"`); - await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "surrenderedUserId" TO "surrendered"`); - } -} diff --git a/packages/backend/migration/1705794768153-reversi-4.js b/packages/backend/migration/1705794768153-reversi-4.js deleted file mode 100644 index 95119cabba..0000000000 --- a/packages/backend/migration/1705794768153-reversi-4.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi41705794768153 { - name = 'Reversi41705794768153' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "endedAt" TIMESTAMP WITH TIME ZONE`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."endedAt" IS 'The ended date of the ReversiGame.'`); - } - - async down(queryRunner) { - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."endedAt" IS 'The ended date of the ReversiGame.'`); - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "endedAt"`); - } -} diff --git a/packages/backend/migration/1705798904141-reversi-5.js b/packages/backend/migration/1705798904141-reversi-5.js deleted file mode 100644 index f1a1a42d46..0000000000 --- a/packages/backend/migration/1705798904141-reversi-5.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi51705798904141 { - name = 'Reversi51705798904141' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "timeLimitForEachTurn" smallint NOT NULL DEFAULT '90'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "timeLimitForEachTurn"`); - } -} diff --git a/packages/backend/migration/1706081514499-reversi-6.js b/packages/backend/migration/1706081514499-reversi-6.js deleted file mode 100644 index 0d9e5cbbf2..0000000000 --- a/packages/backend/migration/1706081514499-reversi-6.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class Reversi61706081514499 { - name = 'Reversi61706081514499' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" ADD "noIrregularRules" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "noIrregularRules"`); - } -} diff --git a/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js b/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js deleted file mode 100644 index 1c45f3756d..0000000000 --- a/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class FixMetaDisableRegistration1706791962000 { - name = 'FixMetaDisableRegistration1706791962000' - - async up(queryRunner) { - await queryRunner.query(`alter table meta alter column "disableRegistration" set default true;`); - } - - async down(queryRunner) { - await queryRunner.query(`alter table meta alter column "disableRegistration" set default false;`); - } -} diff --git a/packages/backend/migration/1707429690000-prohibited-words.js b/packages/backend/migration/1707429690000-prohibited-words.js deleted file mode 100644 index 44e96cb160..0000000000 --- a/packages/backend/migration/1707429690000-prohibited-words.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class prohibitedWords1707429690000 { - name = 'prohibitedWords1707429690000' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWords" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWords"`); - } -} diff --git a/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js b/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js deleted file mode 100644 index 335b14976c..0000000000 --- a/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class MakeRepositoryUrlNullable1707808106310 { - name = 'MakeRepositoryUrlNullable1707808106310' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" DROP NOT NULL`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET NOT NULL`); - } -} diff --git a/packages/backend/migration/1708266695091-repositoryUrl-from-syuilo-to-misskey-dev.js b/packages/backend/migration/1708266695091-repositoryUrl-from-syuilo-to-misskey-dev.js deleted file mode 100644 index 5d9499fadb..0000000000 --- a/packages/backend/migration/1708266695091-repositoryUrl-from-syuilo-to-misskey-dev.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RepositoryUrlFromSyuiloToMisskeyDev1708266695091 { - name = 'RepositoryUrlFromSyuiloToMisskeyDev1708266695091' - - async up(queryRunner) { - await queryRunner.query(`UPDATE "meta" SET "repositoryUrl" = 'https://github.com/kokonect-link/cherrypick' WHERE "repositoryUrl" = 'https://github.com/syuilo/misskey'`); - } - - async down(queryRunner) { - // no valid down migration - } -} diff --git a/packages/backend/migration/1708399372194-per-instance-mod-note.js b/packages/backend/migration/1708399372194-per-instance-mod-note.js deleted file mode 100644 index 339a4d7af9..0000000000 --- a/packages/backend/migration/1708399372194-per-instance-mod-note.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class PerInstanceModNote1708399372194 { - name = 'PerInstanceModNote1708399372194' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ADD "moderationNote" character varying(16384) NOT NULL DEFAULT ''`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "moderationNote"`); - } -} diff --git a/packages/backend/migration/1710210658917-AddSomeUrl.js b/packages/backend/migration/1710210658917-AddSomeUrl.js deleted file mode 100644 index c3d27f269b..0000000000 --- a/packages/backend/migration/1710210658917-AddSomeUrl.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project, noridev, cherrypick-project, esurio - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AddSomeUrl1710210658917 { - name = 'AddSomeUrl1710210658917' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "statusUrl" character varying(1024)`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "statusUrl"`); - } -} diff --git a/packages/backend/migration/1710512074000-url-preview-meta.js b/packages/backend/migration/1710512074000-url-preview-meta.js deleted file mode 100644 index 8af521bbf4..0000000000 --- a/packages/backend/migration/1710512074000-url-preview-meta.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UrlPreviewMeta1710512074000 { - name = 'UrlPreviewMeta1710512074000' - - async up(queryRunner) { - await queryRunner.query(` - alter table meta - rename column "summalyProxy" to "urlPreviewSummaryProxyUrl"; - alter table meta - add "urlPreviewEnabled" boolean default true not null; - alter table meta - add "urlPreviewTimeout" integer default 10000 not null; - alter table meta - add "urlPreviewMaximumContentLength" bigint default 10485760 not null; - alter table meta - add "urlPreviewRequireContentLength" boolean default false not null; - alter table meta - add "urlPreviewUserAgent" varchar(1024) default null; - `); - } - - async down(queryRunner) { - await queryRunner.query(` - alter table meta - rename column "urlPreviewSummaryProxyUrl" to "summalyProxy"; - alter table meta - drop column "urlPreviewEnabled"; - alter table meta - drop column "urlPreviewTimeout"; - alter table meta - drop column "urlPreviewMaximumContentLength"; - alter table meta - drop column "urlPreviewRequireContentLength"; - alter table meta - drop column "urlPreviewUserAgent"; - `); - } -} diff --git a/packages/backend/migration/1710919614510-antenna-exclude-bots.js b/packages/backend/migration/1710919614510-antenna-exclude-bots.js deleted file mode 100644 index fac84317cc..0000000000 --- a/packages/backend/migration/1710919614510-antenna-exclude-bots.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AntennaExcludeBots1710919614510 { - name = 'AntennaExcludeBots1710919614510' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeBots" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeBots"`); - } -} diff --git a/packages/backend/migration/1711722198590-no-recursive-delete.js b/packages/backend/migration/1711722198590-no-recursive-delete.js deleted file mode 100644 index 6fd69826d6..0000000000 --- a/packages/backend/migration/1711722198590-no-recursive-delete.js +++ /dev/null @@ -1,14 +0,0 @@ - -export class NoRecursiveDelete1711722198590 { - name = 'NoRecursiveDelete1711722198590' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`); - await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`); - await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } -} diff --git a/packages/backend/migration/1713656541000-abuse-report-notification.js b/packages/backend/migration/1713656541000-abuse-report-notification.js deleted file mode 100644 index 4a754f81e2..0000000000 --- a/packages/backend/migration/1713656541000-abuse-report-notification.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AbuseReportNotification1713656541000 { - name = 'AbuseReportNotification1713656541000' - - async up(queryRunner) { - await queryRunner.query(` - CREATE TABLE "system_webhook" ( - "id" varchar(32) NOT NULL, - "isActive" boolean NOT NULL DEFAULT true, - "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - "latestSentAt" timestamp with time zone NULL DEFAULT NULL, - "latestStatus" integer NULL DEFAULT NULL, - "name" varchar(255) NOT NULL, - "on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[], - "url" varchar(1024) NOT NULL, - "secret" varchar(1024) NOT NULL, - CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id") - ); - CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive"); - CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on"); - - CREATE TABLE "abuse_report_notification_recipient" ( - "id" varchar(32) NOT NULL, - "isActive" boolean NOT NULL DEFAULT true, - "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - "name" varchar(255) NOT NULL, - "method" varchar(64) NOT NULL, - "userId" varchar(32) NULL DEFAULT NULL, - "systemWebhookId" varchar(32) NULL DEFAULT NULL, - CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"), - CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION, - CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION, - CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION - ); - CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive"); - CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method"); - CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId"); - CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId"); - `); - } - - async down(queryRunner) { - await queryRunner.query(` - ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1"; - ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2"; - ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId"; - DROP INDEX "IDX_abuse_report_notification_recipient_isActive"; - DROP INDEX "IDX_abuse_report_notification_recipient_method"; - DROP INDEX "IDX_abuse_report_notification_recipient_userId"; - DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId"; - DROP TABLE "abuse_report_notification_recipient"; - - DROP INDEX "IDX_system_webhook_isActive"; - DROP INDEX "IDX_system_webhook_on"; - DROP TABLE "system_webhook"; - `); - } -} diff --git a/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js b/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js deleted file mode 100644 index f736378c04..0000000000 --- a/packages/backend/migration/1716129964060-ChannelIdDenormalizedForMiPoll.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class ChannelIdDenormalizedForMiPoll1716129964060 { - name = 'ChannelIdDenormalizedForMiPoll1716129964060' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "poll" ADD "channelId" character varying(32)`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`); - await queryRunner.query(`CREATE INDEX "IDX_c1240fcc9675946ea5d6c2860e" ON "poll" ("channelId") `); - await queryRunner.query(`UPDATE "poll" SET "channelId" = "note"."channelId" FROM "note" WHERE "poll"."noteId" = "note"."id"`); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_c1240fcc9675946ea5d6c2860e"`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`); - await queryRunner.query(`ALTER TABLE "poll" DROP COLUMN "channelId"`); - } -} diff --git a/packages/backend/migration/1716197366117-MediaSilenceForHosts.js b/packages/backend/migration/1716197366117-MediaSilenceForHosts.js deleted file mode 100644 index 10bb7f0255..0000000000 --- a/packages/backend/migration/1716197366117-MediaSilenceForHosts.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class MediaSilenceForHosts1716197366117 { - name = 'MediaSilenceForHosts1716197366117' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`); - } -} diff --git a/packages/backend/migration/1716345015347-NotRespondingSince.js b/packages/backend/migration/1716345015347-NotRespondingSince.js deleted file mode 100644 index fc4ee6639a..0000000000 --- a/packages/backend/migration/1716345015347-NotRespondingSince.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class NotRespondingSince1716345015347 { - name = 'NotRespondingSince1716345015347' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ADD "notRespondingSince" TIMESTAMP WITH TIME ZONE`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "notRespondingSince"`); - } -} diff --git a/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js b/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js deleted file mode 100644 index 4808a9a3db..0000000000 --- a/packages/backend/migration/1716447138870-SuspensionStateInsteadOfIsSspended.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SuspensionStateInsteadOfIsSspended1716345771510 { - name = 'SuspensionStateInsteadOfIsSspended1716345771510' - - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."instance_suspensionstate_enum" AS ENUM('none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding')`); - - await queryRunner.query(`DROP INDEX "public"."IDX_34500da2e38ac393f7bb6b299c"`); - - await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "isSuspended" TO "suspensionState"`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE "public"."instance_suspensionstate_enum" USING ( - CASE "suspensionState" - WHEN TRUE THEN 'manuallySuspended'::instance_suspensionstate_enum - ELSE 'none'::instance_suspensionstate_enum - END - )`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT 'none'`); - - await queryRunner.query(`CREATE INDEX "IDX_3ede46f507c87ad698051d56a8" ON "instance" ("suspensionState") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_3ede46f507c87ad698051d56a8"`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE boolean USING ( - CASE "suspensionState" - WHEN 'none'::instance_suspensionstate_enum THEN FALSE - ELSE TRUE - END - )`); - - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT false`); - - await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "suspensionState" TO "isSuspended"`); - - await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `); - - await queryRunner.query(`DROP TYPE "public"."instance_suspensionstate_enum"`); - } -} diff --git a/packages/backend/migration/1716450883149-RemoveAntennaNotify.js b/packages/backend/migration/1716450883149-RemoveAntennaNotify.js deleted file mode 100644 index b5a2441855..0000000000 --- a/packages/backend/migration/1716450883149-RemoveAntennaNotify.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class RemoveAntennaNotify1716450883149 { - name = 'RemoveAntennaNotify1716450883149' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "notify"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "notify" boolean NOT NULL`); - } -} diff --git a/packages/backend/migration/1717117195275-inquiryUrl.js b/packages/backend/migration/1717117195275-inquiryUrl.js deleted file mode 100644 index 29ca31af14..0000000000 --- a/packages/backend/migration/1717117195275-inquiryUrl.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class InquiryUrl1717117195275 { - name = 'InquiryUrl1717117195275' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "inquiryUrl" character varying(1024)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "inquiryUrl"`); - } -} diff --git a/packages/backend/migration/1717644139656-addDirectSummalyProxy.js b/packages/backend/migration/1717644139656-addDirectSummalyProxy.js deleted file mode 100644 index 06ecbc6acd..0000000000 --- a/packages/backend/migration/1717644139656-addDirectSummalyProxy.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project, noridev, cherrypick-project, esurio - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class addDirectSummalyProxy1717644139656 { - name = 'addDirectSummalyProxy1717644139656' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "directSummalyProxy" boolean NOT NULL DEFAULT false`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "directSummalyProxy"`); - } -} diff --git a/packages/backend/migration/1720161864577-AddDeleteAt.js b/packages/backend/migration/1720161864577-AddDeleteAt.js deleted file mode 100644 index 9257fae14e..0000000000 --- a/packages/backend/migration/1720161864577-AddDeleteAt.js +++ /dev/null @@ -1,11 +0,0 @@ -export class AddDeleteAt1720161864577 { - name = 'AddDeleteAt1720161864577' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "deleteAt" TIMESTAMP WITH TIME ZONE`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "deleteAt"`) - } -} diff --git a/packages/backend/migration/1721299883211-AddIsIndexable.js b/packages/backend/migration/1721299883211-AddIsIndexable.js deleted file mode 100644 index c16566fb2f..0000000000 --- a/packages/backend/migration/1721299883211-AddIsIndexable.js +++ /dev/null @@ -1,13 +0,0 @@ -export class AddIsIndexable1721299883211 { - name = 'AddIsIndexable1721299883211' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "isIndexable" boolean NOT NULL DEFAULT true`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "isIndexable" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isIndexable"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "isIndexable"`); - } -} diff --git a/packages/backend/migration/1721550461923-AddPrivateVisibility.js b/packages/backend/migration/1721550461923-AddPrivateVisibility.js deleted file mode 100644 index ac00bdb10d..0000000000 --- a/packages/backend/migration/1721550461923-AddPrivateVisibility.js +++ /dev/null @@ -1,11 +0,0 @@ -export class AddPrivateVisibility1721550461923 { - name = 'AddPrivateVisibility1721550461923' - - async up(queryRunner) { - await queryRunner.query(`ALTER TYPE "note_visibility_enum" ADD VALUE 'private'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TYPE "note_visibility_enum" DROP VALUE 'private'`); - } -} diff --git a/packages/backend/migration/1721666053703-fixDriveUrl.js b/packages/backend/migration/1721666053703-fixDriveUrl.js deleted file mode 100644 index d8512fb835..0000000000 --- a/packages/backend/migration/1721666053703-fixDriveUrl.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class FixDriveUrl1721666053703 { - name = 'FixDriveUrl1721666053703' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(1024), ALTER COLUMN "url" SET NOT NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`); - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(1024)`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`); - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(1024)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(512)`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`); - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(512)`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`); - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(512), ALTER COLUMN "url" SET NOT NULL`); - } -} diff --git a/packages/backend/migration/1722350613009-AddIsSensitive.js b/packages/backend/migration/1722350613009-AddIsSensitive.js deleted file mode 100644 index a4e313107f..0000000000 --- a/packages/backend/migration/1722350613009-AddIsSensitive.js +++ /dev/null @@ -1,11 +0,0 @@ -export class AddIsSensitive1722350613009 { - name = 'AddIsSensitive1722350613009' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "isSensitive" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isSensitive"`); - } -} diff --git a/packages/backend/migration/1722353293595-AddSensitiveProfile.js b/packages/backend/migration/1722353293595-AddSensitiveProfile.js deleted file mode 100644 index 23dcdc672e..0000000000 --- a/packages/backend/migration/1722353293595-AddSensitiveProfile.js +++ /dev/null @@ -1,11 +0,0 @@ -export class AddSensitiveProfile1722353293595 { - name = 'AddSensitiveProfile1722353293595' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "isSensitive" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "isSensitive"`); - } -} diff --git a/packages/backend/package.json b/packages/backend/package.json index 2d9f3016cf..efa1fd6fa1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,36 +4,29 @@ "private": true, "type": "module", "engines": { - "node": "^20.10.0 || ^22.0.0" + "node": ">=20.10.0" }, "scripts": { "start": "node ./built/boot/entry.js", - "start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js", + "start:test": "NODE_ENV=test node ./built/boot/entry.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js", "revert": "pnpm typeorm migration:revert -d ormconfig.js", - "check:connect": "node ./scripts/check_connect.js", - "build": "swc src -d built -D --strip-leading-paths", - "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths", - "watch:swc": "swc src -d built -D -w --strip-leading-paths", + "check:connect": "node ./check_connect.js", + "build": "swc src -d built -D", + "watch:swc": "swc src -d built -D -w", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", - "watch": "node ./scripts/watch.mjs", + "watch": "node watch.mjs", "restart": "pnpm build && pnpm start", - "dev": "node ./scripts/dev.mjs", + "dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"", "typecheck": "tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.ts\"", - "lint": "pnpm typecheck && pnpm biome lint", - "format": "pnpm biome format", - "format:write": "pnpm biome format --write", - "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs", - "jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs", - "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs", - "jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs", + "lint": "pnpm typecheck && pnpm eslint", + "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit", + "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit", "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "test": "pnpm jest", - "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", "test-and-coverage": "pnpm jest-and-coverage", - "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", - "generate-api-json": "node ./scripts/generate_api_json.js", + "generate-api-json": "node ./generate_api_json.js", "schema:sync": "pnpm typeorm schema:sync -d ormconfig.js" }, "optionalDependencies": { @@ -68,185 +61,178 @@ "utf-8-validate": "6.0.3" }, "dependencies": { - "@aws-sdk/client-s3": "3.620.0", - "@aws-sdk/lib-storage": "3.620.0", - "@bull-board/api": "5.21.1", - "@bull-board/fastify": "5.21.1", - "@bull-board/ui": "5.21.1", - "@discordapp/twemoji": "15.0.3", + "@aws-sdk/client-s3": "3.412.0", + "@aws-sdk/lib-storage": "3.412.0", + "@bull-board/api": "5.10.2", + "@bull-board/fastify": "5.10.2", + "@bull-board/ui": "5.10.2", + "@discordapp/twemoji": "15.0.2", "@fastify/accepts": "4.3.0", - "@fastify/cookie": "9.3.1", - "@fastify/cors": "9.0.1", - "@fastify/express": "3.0.0", - "@fastify/http-proxy": "9.5.0", - "@fastify/multipart": "8.3.0", - "@fastify/rate-limit": "^9.1.0", - "@fastify/static": "7.0.4", - "@fastify/view": "9.1.0", + "@fastify/cookie": "9.2.0", + "@fastify/cors": "8.5.0", + "@fastify/express": "2.3.0", + "@fastify/http-proxy": "9.3.0", + "@fastify/multipart": "8.0.0", + "@fastify/static": "6.12.0", + "@fastify/view": "8.2.0", "@google-cloud/logging": "^10.5.0", "@google-cloud/translate": "^7.2.1", - "@misskey-dev/sharp-read-bmp": "1.2.0", - "@misskey-dev/summaly": "5.1.0", - "@napi-rs/canvas": "^0.1.53", - "@nestjs/common": "10.3.10", - "@nestjs/core": "10.3.10", - "@nestjs/testing": "10.3.10", - "@opensearch-project/opensearch": "^2.7.0", + "@nestjs/common": "10.2.10", + "@nestjs/core": "10.2.10", + "@nestjs/testing": "10.2.10", "@peertube/http-signature": "1.7.0", - "@sentry/node": "8.20.0", - "@sentry/profiling-node": "8.20.0", - "@simplewebauthn/server": "10.0.1", + "@simplewebauthn/server": "8.3.5", "@sinonjs/fake-timers": "11.2.2", - "@smithy/node-http-handler": "2.5.0", - "@swc/cli": "0.3.12", - "@swc/core": "1.6.6", - "@twemoji/parser": "15.1.1", + "@smithy/node-http-handler": "2.1.10", + "@swc/cli": "0.1.63", + "@swc/core": "1.3.100", + "@twemoji/parser": "15.0.0", "@vitalets/google-translate-api": "9.2.0", "accepts": "1.3.8", - "ajv": "8.17.1", - "archiver": "7.0.1", - "argon2": "^0.40.3", - "async-mutex": "0.5.0", + "ajv": "8.12.0", + "archiver": "6.0.1", + "async-mutex": "0.4.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "5.10.4", + "bullmq": "4.15.4", "cacheable-lookup": "7.0.0", - "cbor": "9.0.2", + "cbor": "9.0.1", "chalk": "5.3.0", "chalk-template": "1.1.0", "cherrypick-js": "workspace:*", "cherrypick-mfm-js": "0.24.0-cherrypick.4", - "chokidar": "3.6.0", + "chokidar": "3.5.3", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "4.28.1", + "fastify": "4.24.3", "fastify-raw-body": "4.3.0", "feed": "4.2.2", - "file-type": "19.3.0", - "fluent-ffmpeg": "2.1.3", + "file-type": "18.7.0", + "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", - "got": "14.4.2", + "got": "14.0.0", "happy-dom": "10.0.3", "hpagent": "1.2.0", - "htmlescape": "1.1.1", - "http-link-header": "1.1.3", - "ioredis": "5.4.1", - "ip-cidr": "4.0.1", - "ipaddr.js": "2.2.0", - "is-svg": "5.0.1", + "http-link-header": "1.1.1", + "ioredis": "5.3.2", + "ip-cidr": "3.1.0", + "ipaddr.js": "2.1.0", + "is-svg": "5.0.0", "js-yaml": "4.1.0", - "jsdom": "24.1.1", + "jsdom": "23.0.1", "json5": "2.2.3", "jsonld": "8.3.2", - "jsrsasign": "11.1.0", - "meilisearch": "0.41.0", + "jsrsasign": "10.9.0", + "meilisearch": "0.36.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", "ms": "3.0.0-canary.1", - "nanoid": "5.0.7", + "nanoid": "5.0.4", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.14", + "nodemailer": "6.9.7", "nsfwjs": "2.4.2", "oauth": "0.10.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.3.1", + "otpauth": "9.2.1", "parse5": "7.1.2", - "pg": "8.12.0", - "pkce-challenge": "4.1.0", + "pg": "8.11.3", + "pkce-challenge": "4.0.1", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", - "pug": "3.0.3", + "pug": "3.0.2", "punycode": "2.3.1", + "pureimage": "0.3.17", "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.21.3", + "re2": "1.20.9", "redis-lock": "0.1.4", - "reflect-metadata": "0.2.2", + "reflect-metadata": "0.1.14", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.1", - "sanitize-html": "2.13.0", + "sanitize-html": "2.11.0", "secure-json-parse": "2.7.0", - "sharp": "0.33.4", + "sharp": "0.32.6", + "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "strip-ansi": "^7.1.0", - "systeminformation": "5.22.11", + "summaly": "github:misskey-dev/summaly", + "systeminformation": "5.21.20", "tinycolor2": "1.6.0", - "tmp": "0.2.3", - "tsc-alias": "1.8.10", + "tmp": "0.2.1", + "tsc-alias": "1.8.8", "tsconfig-paths": "4.2.0", - "typeorm": "0.3.20", - "typescript": "5.5.4", + "typeorm": "0.3.17", + "typescript": "5.3.3", "ulid": "2.3.0", "vary": "1.1.2", - "web-push": "3.6.7", - "ws": "8.18.0", + "web-push": "3.6.6", + "ws": "8.15.1", "xev": "3.0.2" }, "devDependencies": { - "@biomejs/biome": "1.8.3", "@jest/globals": "29.7.0", - "@nestjs/platform-express": "10.3.10", - "@simplewebauthn/types": "10.0.0", - "@swc/jest": "0.2.36", + "@simplewebauthn/typescript-types": "8.3.4", + "@swc/jest": "0.2.29", "@types/accepts": "1.3.7", "@types/archiver": "6.0.2", "@types/bcryptjs": "2.4.6", "@types/body-parser": "1.19.5", + "@types/cbor": "6.0.0", "@types/color-convert": "2.0.3", "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.24", - "@types/htmlescape": "1.1.3", - "@types/http-link-header": "1.0.7", - "@types/jest": "29.5.12", + "@types/http-link-header": "1.0.5", + "@types/jest": "29.5.11", "@types/js-yaml": "4.0.9", - "@types/jsdom": "21.1.7", - "@types/jsonld": "1.5.15", - "@types/jsrsasign": "10.5.14", + "@types/jsdom": "21.1.6", + "@types/jsonld": "1.5.13", + "@types/jsrsasign": "10.5.12", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.14.12", - "@types/nodemailer": "6.4.15", - "@types/oauth": "0.9.5", - "@types/oauth2orize": "1.11.5", + "@types/node": "20.10.5", + "@types/node-fetch": "3.0.3", + "@types/nodemailer": "6.4.14", + "@types/oauth": "0.9.4", + "@types/oauth2orize": "1.11.3", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.11.6", + "@types/pg": "8.10.9", "@types/pug": "2.0.10", - "@types/punycode": "2.1.4", + "@types/punycode": "2.1.3", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.11.0", - "@types/semver": "7.5.8", + "@types/sanitize-html": "2.9.5", + "@types/semver": "7.5.6", + "@types/sharp": "0.32.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", "@types/tinycolor2": "1.4.6", "@types/tmp": "0.2.6", "@types/vary": "1.1.3", "@types/web-push": "3.6.3", - "@types/ws": "8.5.11", - "@typescript-eslint/eslint-plugin": "7.17.0", - "@typescript-eslint/parser": "7.17.0", - "aws-sdk-client-mock": "4.0.1", + "@types/ws": "8.5.10", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", + "aws-sdk-client-mock": "3.0.0", "cross-env": "7.0.3", + "eslint": "8.56.0", "eslint-plugin-import": "2.29.1", - "execa": "9.3.0", - "fkill": "9.0.0", + "execa": "8.0.1", "jest": "29.7.0", "jest-mock": "29.7.0", - "nodemon": "3.1.4", - "pid-port": "1.0.0", - "simple-oauth2": "5.1.0" + "nodemon": "3.0.2", + "simple-oauth2": "5.0.0" } } diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs deleted file mode 100644 index a3e0558abd..0000000000 --- a/packages/backend/scripts/dev.mjs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { execa, execaNode } from 'execa'; - -/** @type {import('execa').ExecaChildProcess | undefined} */ -let backendProcess; - -async function execBuildAssets() { - await execa('pnpm', ['run', 'build-assets'], { - cwd: '../../', - stdout: process.stdout, - stderr: process.stderr, - }) -} - -function execStart() { - // pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので - // 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい - backendProcess = execaNode('./built/boot/entry.js', [], { - stdout: process.stdout, - stderr: process.stderr, - env: { - 'NODE_ENV': 'development', - }, - }); -} - -async function killProc() { - if (backendProcess) { - backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す - backendProcess.kill(); - await new Promise(resolve => backendProcess.on('exit', resolve)); - backendProcess = undefined; - } -} - -(async () => { - execaNode( - './node_modules/nodemon/bin/nodemon.js', - [ - '-w', 'src', - '-e', 'ts,js,mjs,cjs,json', - '--exec', 'pnpm', 'run', 'build', - ], - { - stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], - serialization: "json", - }) - .on('message', async (message) => { - if (message.type === 'exit') { - // かならずbuild->build-assetsの順番で呼び出したいので、 - // 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。 - // pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある - - await killProc(); - await execBuildAssets(); - execStart(); - } - }) -})(); diff --git a/packages/backend/scripts/generate_api_json.js b/packages/backend/scripts/generate_api_json.js deleted file mode 100644 index 798e243004..0000000000 --- a/packages/backend/scripts/generate_api_json.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { execa } from 'execa'; -import { writeFileSync, existsSync } from "node:fs"; - -async function main() { - if (!process.argv.includes('--no-build')) { - await execa('pnpm', ['run', 'build'], { - stdout: process.stdout, - stderr: process.stderr, - }); - } - - if (!existsSync('./built')) { - throw new Error('`built` directory does not exist.'); - } - - /** @type {import('../src/config.js')} */ - const { loadConfig } = await import('../built/config.js'); - - /** @type {import('../src/server/api/openapi/gen-spec.js')} */ - const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js'); - - const config = loadConfig(); - const spec = genOpenapiSpec(config, true); - - writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); -} - -main().catch(e => { - console.error(e); - process.exit(1); -}); diff --git a/packages/backend/src/@types/hcaptcha.d.ts b/packages/backend/src/@types/hcaptcha.d.ts index e11dda4662..f2250542ee 100644 --- a/packages/backend/src/@types/hcaptcha.d.ts +++ b/packages/backend/src/@types/hcaptcha.d.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts index 75b62e55f0..6c11167fcc 100644 --- a/packages/backend/src/@types/http-signature.d.ts +++ b/packages/backend/src/@types/http-signature.d.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/@types/os-utils.d.ts b/packages/backend/src/@types/os-utils.d.ts index 8943edddd1..230d3c115f 100644 --- a/packages/backend/src/@types/os-utils.d.ts +++ b/packages/backend/src/@types/os-utils.d.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/@types/package.json.d.ts b/packages/backend/src/@types/package.json.d.ts index 52a2b356db..c48d64f2c4 100644 --- a/packages/backend/src/@types/package.json.d.ts +++ b/packages/backend/src/@types/package.json.d.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/@types/probe-image-size.d.ts b/packages/backend/src/@types/probe-image-size.d.ts index 538836475c..350b97c921 100644 --- a/packages/backend/src/@types/probe-image-size.d.ts +++ b/packages/backend/src/@types/probe-image-size.d.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/@types/redis-lock.d.ts b/packages/backend/src/@types/redis-lock.d.ts index b037cde5ee..5576c4fe57 100644 --- a/packages/backend/src/@types/redis-lock.d.ts +++ b/packages/backend/src/@types/redis-lock.d.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index f4ebdd3527..6f850e857d 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -1,20 +1,19 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ +import { setTimeout } from 'node:timers/promises'; import process from 'node:process'; import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; import { MeiliSearch } from 'meilisearch'; -import { Client as OpenSearch } from '@opensearch-project/opensearch'; import { Logging } from '@google-cloud/logging'; 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'; const $config: Provider = { @@ -36,7 +35,7 @@ const $meilisearch: Provider = { useFactory: (config: Config) => { if (config.meilisearch) { return new MeiliSearch({ - host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`, + host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`, apiKey: config.meilisearch.apiKey, }); } else { @@ -46,29 +45,6 @@ const $meilisearch: Provider = { inject: [DI.config], }; -const $opensearch: Provider = { - provide: DI.opensearch, - useFactory: (config: Config) => { - if (config.opensearch) { - return new OpenSearch({ - nodes: { - url: new URL(`${config.opensearch.ssl ? 'https' : 'http'}://${config.opensearch.host}:${config.opensearch.port}`), - ssl: { - rejectUnauthorized: config.opensearch.rejectUnauthorized, - }, - }, - auth: { - username: config.opensearch.user, - password: config.opensearch.pass, - }, - }); - } else { - return null; - } - }, - inject: [DI.config], -}; - const $cloudLogging: Provider = { provide: DI.cloudLogging, useFactory: (config: Config) => { @@ -133,8 +109,8 @@ const $redisForJobQueue: Provider = { @Global() @Module({ imports: [RepositoryModule], - providers: [$config, $db, $meilisearch, $opensearch, $cloudLogging, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForJobQueue], - exports: [$config, $db, $meilisearch, $opensearch, $cloudLogging, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForJobQueue, RepositoryModule], + providers: [$config, $db, $meilisearch, $cloudLogging, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForJobQueue], + exports: [$config, $db, $meilisearch, $cloudLogging, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForJobQueue, RepositoryModule], }) export class GlobalModule implements OnApplicationShutdown { constructor( @@ -144,12 +120,17 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @Inject(DI.redisForJobQueue) private redisForJobQueue: Redis.Redis, - ) { } + ) {} public async dispose(): Promise { - // Wait for all potential DB queries - await allSettled(); - // And then disconnect from DB + if (process.env.NODE_ENV === 'test') { + // XXX: + // Shutting down the existing connections causes errors on Jest as + // Misskey has asynchronous postgres/redis connections that are not + // awaited. + // Let's wait for some random time for them to finish. + await setTimeout(5000); + } await Promise.all([ this.db.destroy(), this.redisClient.disconnect(), diff --git a/packages/backend/src/MainModule.ts b/packages/backend/src/MainModule.ts index f86a0be93c..baa9dffd16 100644 --- a/packages/backend/src/MainModule.ts +++ b/packages/backend/src/MainModule.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts index d0be19664f..c73a6f2cb5 100644 --- a/packages/backend/src/NestLogger.ts +++ b/packages/backend/src/NestLogger.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common'; import Logger from '@/logger.js'; const logger = new Logger('core', 'cyan'); -const nestLogger = logger.createSubLogger('nest', 'green'); +const nestLogger = logger.createSubLogger('nest', 'green', false); export class NestLogger implements LoggerService { /** diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts index bead4e2859..1e00375bbd 100644 --- a/packages/backend/src/boot/common.ts +++ b/packages/backend/src/boot/common.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index 13651a6193..caea39b9b6 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -16,7 +16,6 @@ import Logger from '@/logger.js'; import { envOption } from '../env.js'; import { masterMain } from './master.js'; import { workerMain } from './worker.js'; -import { readyRef } from './ready.js'; import 'reflect-metadata'; @@ -26,7 +25,7 @@ Error.stackTraceLimit = Infinity; EventEmitter.defaultMaxListeners = 128; const logger = new Logger('core', 'cyan'); -const clusterLogger = logger.createSubLogger('cluster', 'orange'); +const clusterLogger = logger.createSubLogger('cluster', 'orange', false); const ev = new Xev(); //#region Events @@ -103,8 +102,6 @@ if (cluster.isWorker || envOption.disableClustering) { await workerMain(); } -readyRef.value = true; - // ユニットテスト時にMisskeyが子プロセスで起動された時のため // それ以外のときは process.send は使えないので弾く if (process.send) { diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 73c91c4623..b9e83a8310 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,8 +10,6 @@ import * as os from 'node:os'; import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; -import * as Sentry from '@sentry/node'; -import { nodeProfilingIntegration } from '@sentry/profiling-node'; import Logger from '@/logger.js'; import { loadConfig } from '@/config.js'; import type { Config } from '@/config.js'; @@ -25,7 +23,7 @@ const _dirname = dirname(_filename); const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const logger = new Logger('core', 'cyan'); -const bootLogger = logger.createSubLogger('boot', 'magenta'); +const bootLogger = logger.createSubLogger('boot', 'magenta', false); const themeColor = chalk.hex('#ffa9c3'); @@ -76,24 +74,6 @@ export async function masterMain() { bootLogger.succ(chalk.hex('#ffa9c3')('Cherry') + chalk.hex('#95e3e8')('Pick') + (' initialized')); - if (config.sentryForBackend) { - Sentry.init({ - integrations: [ - ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []), - ], - - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions - - // Set sampling rate for profiling - this is relative to tracesSampleRate - profilesSampleRate: 1.0, - - maxBreadcrumbs: 0, - - ...config.sentryForBackend.options, - }); - } - if (envOption.disableClustering) { if (envOption.onlyServer) { await server(); diff --git a/packages/backend/src/boot/ready.ts b/packages/backend/src/boot/ready.ts deleted file mode 100644 index 591ae5cb58..0000000000 --- a/packages/backend/src/boot/ready.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const readyRef = { value: false }; diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index 5d4a15b29f..6c7b2fb475 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -1,39 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import cluster from 'node:cluster'; -import * as Sentry from '@sentry/node'; -import { nodeProfilingIntegration } from '@sentry/profiling-node'; import { envOption } from '@/env.js'; -import { loadConfig } from '@/config.js'; import { jobQueue, server } from './common.js'; /** * Init worker process */ export async function workerMain() { - const config = loadConfig(); - - if (config.sentryForBackend) { - Sentry.init({ - integrations: [ - ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []), - ], - - // Performance Monitoring - tracesSampleRate: 1.0, // Capture 100% of the transactions - - // Set sampling rate for profiling - this is relative to tracesSampleRate - profilesSampleRate: 1.0, - - maxBreadcrumbs: 0, - - ...config.sentryForBackend.options, - }); - } - if (envOption.onlyServer) { await server(); } else if (envOption.onlyQueue) { diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 8b2ac9c88c..f491239e0c 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,6 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; -import * as Sentry from '@sentry/node'; import type { RedisOptions } from 'ioredis'; type RedisOptionsSource = Partial & { @@ -23,7 +22,7 @@ type RedisOptionsSource = Partial & { * 設定ファイルの型 */ type Source = { - url?: string; + url: string; port?: number; socket?: string; chmodSocket?: string; @@ -31,9 +30,9 @@ type Source = { db: { host: string; port: number; - db?: string; - user?: string; - pass?: string; + db: string; + user: string; + pass: string; disableCache?: boolean; extra?: { [x: string]: string }; }; @@ -45,7 +44,6 @@ type Source = { user: string; pass: string; }[]; - pgroonga?: boolean; redis: RedisOptionsSource; redisForPubsub?: RedisOptionsSource; redisForJobQueue?: RedisOptionsSource; @@ -58,19 +56,6 @@ type Source = { index: string; scope?: 'local' | 'global' | string[]; }; - opensearch?: { - host: string; - port: string; - user: string; - pass: string; - ssl?: boolean; - rejectUnauthorized?: boolean; - index: string; - } | undefined; - sentryForBackend?: { options: Partial; enableNodeProfiling: boolean; }; - sentryForFrontend?: { options: Partial }; - - publishTarballInsteadOfProvideRepositoryUrl?: boolean; proxy?: string; proxySmtp?: string; @@ -89,10 +74,10 @@ type Source = { deliverJobConcurrency?: number; inboxJobConcurrency?: number; - relationshipJobConcurrency?: number; + relashionshipJobConcurrency?: number; deliverJobPerSec?: number; inboxJobPerSec?: number; - relationshipJobPerSec?: number; + relashionshipJobPerSec?: number; deliverJobMaxAttempts?: number; inboxJobMaxAttempts?: number; @@ -105,7 +90,6 @@ type Source = { apFileBaseUrl?: string; mediaProxy?: string; - remoteProxy?: string; proxyRemoteFiles?: boolean; videoThumbnailGenerator?: string; @@ -140,7 +124,6 @@ export type Config = { user: string; pass: string; }[] | undefined; - pgroonga: boolean | undefined; meilisearch: { host: string; port: string; @@ -149,15 +132,6 @@ export type Config = { index: string; scope?: 'local' | 'global' | string[]; } | undefined; - opensearch: { - host: string; - port: string; - user: string; - pass: string; - ssl?: boolean; - rejectUnauthorized?: boolean; - index: string; - } | undefined; proxy: string | undefined; proxySmtp: string | undefined; proxyBypassHosts: string[] | undefined; @@ -169,10 +143,10 @@ export type Config = { outgoingAddressFamily: 'ipv4' | 'ipv6' | 'dual' | undefined; deliverJobConcurrency: number | undefined; inboxJobConcurrency: number | undefined; - relationshipJobConcurrency: number | undefined; + relashionshipJobConcurrency: number | undefined; deliverJobPerSec: number | undefined; inboxJobPerSec: number | undefined; - relationshipJobPerSec: number | undefined; + relashionshipJobPerSec: number | undefined; deliverJobMaxAttempts: number | undefined; inboxJobMaxAttempts: number | undefined; @@ -188,7 +162,6 @@ export type Config = { version: string; basedMisskeyVersion: string; - publishTarballInsteadOfProvideRepositoryUrl: boolean; host: string; hostname: string; scheme: string; @@ -201,15 +174,12 @@ export type Config = { clientEntry: string; clientManifestExists: boolean; mediaProxy: string; - remoteProxy?: string; externalMediaProxyEnabled: boolean; videoThumbnailGenerator: string | null; redis: RedisOptions & RedisOptionsSource; redisForPubsub: RedisOptions & RedisOptionsSource; redisForJobQueue: RedisOptions & RedisOptionsSource; redisForTimelines: RedisOptions & RedisOptionsSource; - sentryForBackend: { options: Partial; enableNodeProfiling: boolean; } | undefined; - sentryForFrontend: { options: Partial } | undefined; perChannelMaxNoteCacheCount: number; perUserNotificationsMaxCount: number; deactivateAntennaThreshold: number; @@ -241,7 +211,7 @@ export function loadConfig(): Config { : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; - const url = tryCreateUrl(config.url ?? process.env.CHERRYPICK_URL ?? ''); + const url = tryCreateUrl(config.url); const version = meta.version; const basedMisskeyVersion = meta.basedMisskeyVersion; const host = url.host; @@ -249,25 +219,15 @@ export function loadConfig(): Config { const scheme = url.protocol.replace(/:$/, ''); const wsScheme = scheme.replace('http', 'ws'); - const dbDb = config.db.db ?? process.env.DATABASE_DB ?? ''; - const dbUser = config.db.user ?? process.env.DATABASE_USER ?? ''; - const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? ''; - const externalMediaProxy = config.mediaProxy ? config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy : null; const internalMediaProxy = `${scheme}://${host}/proxy`; - - const remoteProxy = config.remoteProxy ? - config.remoteProxy.endsWith('/') ? config.remoteProxy.substring(0, config.remoteProxy.length - 1) : config.remoteProxy - : undefined; - const redis = convertRedisOptions(config.redis, host); return { version, basedMisskeyVersion, - publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, url: url.origin, port: config.port ?? parseInt(process.env.PORT ?? '', 10), socket: config.socket, @@ -281,18 +241,14 @@ export function loadConfig(): Config { apiUrl: `${scheme}://${host}/api`, authUrl: `${scheme}://${host}/auth`, driveUrl: `${scheme}://${host}/files`, - db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass }, + db: config.db, dbReplications: config.dbReplications, dbSlaves: config.dbSlaves, - pgroonga: config.pgroonga, meilisearch: config.meilisearch, - opensearch: config.opensearch, 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, - sentryForBackend: config.sentryForBackend, - sentryForFrontend: config.sentryForFrontend, id: config.id, proxy: config.proxy, proxySmtp: config.proxySmtp, @@ -304,18 +260,17 @@ export function loadConfig(): Config { outgoingAddressFamily: config.outgoingAddressFamily, deliverJobConcurrency: config.deliverJobConcurrency, inboxJobConcurrency: config.inboxJobConcurrency, - relationshipJobConcurrency: config.relationshipJobConcurrency, + relashionshipJobConcurrency: config.relashionshipJobConcurrency, deliverJobPerSec: config.deliverJobPerSec, inboxJobPerSec: config.inboxJobPerSec, - relationshipJobPerSec: config.relationshipJobPerSec, + relashionshipJobPerSec: config.relashionshipJobPerSec, deliverJobMaxAttempts: config.deliverJobMaxAttempts, inboxJobMaxAttempts: config.inboxJobMaxAttempts, proxyRemoteFiles: config.proxyRemoteFiles, - signToActivityPubGet: config.signToActivityPubGet ?? true, + signToActivityPubGet: config.signToActivityPubGet, apFileBaseUrl: config.apFileBaseUrl, mediaProxy: externalMediaProxy ?? internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, - remoteProxy, videoThumbnailGenerator: config.videoThumbnailGenerator ? config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator : null, diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 074305930d..577bb29807 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -1,10 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -// dummy -export const MAX_NOTE_TEXT_LENGTH = 5120; +export const MAX_NOTE_TEXT_LENGTH = 3000; export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts deleted file mode 100644 index 7be5335885..0000000000 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ /dev/null @@ -1,405 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common'; -import { Brackets, In, IsNull, Not } from 'typeorm'; -import * as Redis from 'ioredis'; -import sanitizeHtml from 'sanitize-html'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; -import type { - AbuseReportNotificationRecipientRepository, - MiAbuseReportNotificationRecipient, - MiAbuseUserReport, - MiUser, -} from '@/models/_.js'; -import { EmailService } from '@/core/EmailService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { IdService } from './IdService.js'; - -@Injectable() -export class AbuseReportNotificationService implements OnApplicationShutdown { - constructor( - @Inject(DI.abuseReportNotificationRecipientRepository) - private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - private idService: IdService, - private roleService: RoleService, - private systemWebhookService: SystemWebhookService, - private emailService: EmailService, - private metaService: MetaService, - private moderationLogService: ModerationLogService, - private globalEventService: GlobalEventService, - ) { - this.redisForSub.on('message', this.onMessage); - } - - /** - * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する. - * 通知先ユーザは{@link getModeratorIds}の取得結果に依る. - * - * @see RoleService.getModeratorIds - * @see GlobalEventService.publishAdminStream - */ - @bindThis - public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) { - if (abuseReports.length <= 0) { - return; - } - - const moderatorIds = await this.roleService.getModeratorIds(true, true); - - for (const moderatorId of moderatorIds) { - for (const abuseReport of abuseReports) { - this.globalEventService.publishAdminStream( - moderatorId, - 'newAbuseUserReport', - { - id: abuseReport.id, - targetUserId: abuseReport.targetUserId, - reporterId: abuseReport.reporterId, - comment: abuseReport.comment, - }, - ); - } - } - } - - /** - * Mailを用いて{@link abuseReports}の内容を管理者各位に通知する. - * メールアドレスの送信先は以下の通り. - * - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る) - * - metaテーブルに設定されているメールアドレス - * - * @see EmailService.sendEmail - */ - @bindThis - public async notifyMail(abuseReports: MiAbuseUserReport[]) { - if (abuseReports.length <= 0) { - return; - } - - const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it - .filter(it => it.isActive && it.userProfile?.emailVerified) - .map(it => it.userProfile?.email) - .filter(x => x != null), - ); - - // 送信先の鮮度を保つため、毎回取得する - const meta = await this.metaService.fetch(true); - recipientEMailAddresses.push( - ...(meta.email ? [meta.email] : []), - ); - - if (recipientEMailAddresses.length <= 0) { - return; - } - - for (const mailAddress of recipientEMailAddresses) { - await Promise.all( - abuseReports.map(it => { - // TODO: 送信処理はJobQueue化したい - return this.emailService.sendEmail( - mailAddress, - 'New Abuse Report', - sanitizeHtml(it.comment), - sanitizeHtml(it.comment), - ); - }), - ); - } - } - - /** - * SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する. - * ここではJobQueueへのエンキューのみを行うため、即時実行されない. - * - * @see SystemWebhookService.enqueueSystemWebhook - */ - @bindThis - public async notifySystemWebhook( - abuseReports: MiAbuseUserReport[], - type: 'abuseReport' | 'abuseReportResolved', - ) { - if (abuseReports.length <= 0) { - return; - } - - 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( - abuseReports.map(it => { - return this.systemWebhookService.enqueueSystemWebhook( - webhookId, - type, - it, - ); - }), - ); - } - } - - /** - * 通報の通知先一覧を取得する. - * - * @param {Object} [params] クエリの取得条件 - * @param {Object} [params.method] 取得する通知先の通知方法 - * @param {Object} [opts] 動作時の詳細なオプション - * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true) - * @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false) - * @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false) - * @see removeUnauthorizedRecipientUsers - */ - @bindThis - public async fetchRecipients( - params?: { - ids?: MiAbuseReportNotificationRecipient['id'][], - method?: RecipientMethod[], - }, - opts?: { - removeUnauthorized?: boolean, - joinUser?: boolean, - joinSystemWebhook?: boolean, - }, - ): Promise { - const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient'); - - if (opts?.joinUser) { - query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id'); - query.innerJoinAndSelect('recipient.userProfile', 'userProfile'); - } - - if (opts?.joinSystemWebhook) { - query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook'); - } - - if (params?.ids) { - query.andWhere({ id: In(params.ids) }); - } - - if (params?.method) { - query.andWhere(new Brackets(qb => { - if (params.method?.includes('email')) { - qb.orWhere({ method: 'email', userId: Not(IsNull()) }); - } - if (params.method?.includes('webhook')) { - qb.orWhere({ method: 'webhook', userId: IsNull() }); - } - })); - } - - const recipients = await query.getMany(); - if (recipients.length <= 0) { - return []; - } - - // アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション) - return (opts?.removeUnauthorized ?? true) - ? await this.removeUnauthorizedRecipientUsers(recipients) - : recipients; - } - - /** - * EMailの通知先一覧を取得する. - * リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する. - * - * @param {Object} [opts] - * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true) - * @see removeUnauthorizedRecipientUsers - */ - @bindThis - public async fetchEMailRecipients(opts?: { - removeUnauthorized?: boolean - }): Promise { - return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts }); - } - - /** - * Webhookの通知先一覧を取得する. - * リレーション先の{@link MiSystemWebhook}も同時に取得する. - */ - @bindThis - public fetchWebhookRecipients(): Promise { - return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true }); - } - - /** - * 通知先を作成する. - */ - @bindThis - public async createRecipient( - params: { - isActive: MiAbuseReportNotificationRecipient['isActive']; - name: MiAbuseReportNotificationRecipient['name']; - method: MiAbuseReportNotificationRecipient['method']; - userId: MiAbuseReportNotificationRecipient['userId']; - systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId']; - }, - updater: MiUser, - ): Promise { - const id = this.idService.gen(); - await this.abuseReportNotificationRecipientRepository.insert({ - ...params, - id, - }); - - const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id }); - - this.moderationLogService - .log(updater, 'createAbuseReportNotificationRecipient', { - recipientId: id, - recipient: created, - }) - .then(); - - return created; - } - - /** - * 通知先を更新する. - */ - @bindThis - public async updateRecipient( - params: { - id: MiAbuseReportNotificationRecipient['id']; - isActive: MiAbuseReportNotificationRecipient['isActive']; - name: MiAbuseReportNotificationRecipient['name']; - method: MiAbuseReportNotificationRecipient['method']; - userId: MiAbuseReportNotificationRecipient['userId']; - systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId']; - }, - updater: MiUser, - ): Promise { - const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id }); - - await this.abuseReportNotificationRecipientRepository.update(params.id, { - isActive: params.isActive, - updatedAt: new Date(), - name: params.name, - method: params.method, - userId: params.userId, - systemWebhookId: params.systemWebhookId, - }); - - const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id }); - - this.moderationLogService - .log(updater, 'updateAbuseReportNotificationRecipient', { - recipientId: params.id, - before: beforeEntity, - after: afterEntity, - }) - .then(); - - return afterEntity; - } - - /** - * 通知先を削除する. - */ - @bindThis - public async deleteRecipient( - id: MiAbuseReportNotificationRecipient['id'], - updater: MiUser, - ) { - const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id }); - - await this.abuseReportNotificationRecipientRepository.delete(id); - - this.moderationLogService - .log(updater, 'deleteAbuseReportNotificationRecipient', { - recipientId: id, - recipient: entity, - }) - .then(); - } - - /** - * モデレータ権限を持たない(*1)通知先ユーザを削除する. - * - * *1: 以下の両方を満たすものの事を言う - * - 通知先にユーザIDが設定されている - * - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている - * - * @param recipients 通知先一覧の配列 - * @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列 - */ - @bindThis - private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise { - const userRecipients = recipients.filter(it => it.userId !== null); - const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null)); - if (recipientUserIds.size <= 0) { - // ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い - return recipients; - } - - // モデレータ権限の有無で通知先設定を振り分ける - const authorizedUserIds = await this.roleService.getModeratorIds(true, true); - const authorizedUserRecipients = Array.of(); - const unauthorizedUserRecipients = Array.of(); - for (const recipient of userRecipients) { - // eslint-disable-next-line - if (authorizedUserIds.includes(recipient.userId!)) { - authorizedUserRecipients.push(recipient); - } else { - unauthorizedUserRecipients.push(recipient); - } - } - - // モデレータ権限を持たない通知先をDBから削除する - if (unauthorizedUserRecipients.length > 0) { - await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id)); - } - const nonUserRecipients = recipients.filter(it => it.userId === null); - return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id)); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - if (obj.channel !== 'internal') { - return; - } - - const { type } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'roleUpdated': - case 'roleDeleted': - case 'userRoleUnassigned': { - // 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行 - process.nextTick(async () => { - const recipients = await this.abuseReportNotificationRecipientRepository.findBy({ - userId: Not(IsNull()), - }); - await this.removeUnauthorizedRecipientUsers(recipients); - }); - break; - } - default: { - break; - } - } - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts deleted file mode 100644 index 69c51509ba..0000000000 --- a/packages/backend/src/core/AbuseReportService.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { QueueService } from '@/core/QueueService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; -import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { IdService } from './IdService.js'; - -@Injectable() -export class AbuseReportService { - constructor( - @Inject(DI.abuseUserReportsRepository) - private abuseUserReportsRepository: AbuseUserReportsRepository, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - private idService: IdService, - private abuseReportNotificationService: AbuseReportNotificationService, - private queueService: QueueService, - private instanceActorService: InstanceActorService, - private apRendererService: ApRendererService, - private moderationLogService: ModerationLogService, - ) { - } - - /** - * ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する. - * - 管理者用Redisイベント - * - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス) - * - SystemWebhook - * - * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える - * @see AbuseReportNotificationService.notify - */ - @bindThis - public async report(params: { - targetUserId: MiAbuseUserReport['targetUserId'], - targetUserHost: MiAbuseUserReport['targetUserHost'], - reporterId: MiAbuseUserReport['reporterId'], - reporterHost: MiAbuseUserReport['reporterHost'], - comment: string, - }[]) { - const entities = params.map(param => { - return { - id: this.idService.gen(), - targetUserId: param.targetUserId, - targetUserHost: param.targetUserHost, - reporterId: param.reporterId, - reporterHost: param.reporterHost, - comment: param.comment, - }; - }); - - const reports = Array.of(); - for (const entity of entities) { - const report = await this.abuseUserReportsRepository.insertOne(entity); - reports.push(report); - } - - return Promise.all([ - this.abuseReportNotificationService.notifyAdminStream(reports), - this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'), - this.abuseReportNotificationService.notifyMail(reports), - ]); - } - - /** - * 通報を解決し、その内容を下記の手段で管理者各位に通知する. - * - SystemWebhook - * - * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える - * @param operator 通報を処理したユーザ - * @see AbuseReportNotificationService.notify - */ - @bindThis - public async resolve( - params: { - reportId: string; - forward: boolean; - }[], - operator: MiUser, - ) { - const paramsMap = new Map(params.map(it => [it.reportId, it])); - const reports = await this.abuseUserReportsRepository.findBy({ - id: In(params.map(it => it.reportId)), - }); - - for (const report of reports) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const ps = paramsMap.get(report.id)!; - - await this.abuseUserReportsRepository.update(report.id, { - resolved: true, - assigneeId: operator.id, - forwarded: ps.forward && report.targetUserHost !== null, - }); - - if (ps.forward && report.targetUserHost != null) { - const actor = await this.instanceActorService.getInstanceActor(); - const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); - - // eslint-disable-next-line - const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment); - const contextAssignedFlag = this.apRendererService.addContext(flag); - this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false); - } - - this.moderationLogService - .log(operator, 'resolveAbuseReport', { - reportId: report.id, - report: report, - forwarded: ps.forward && report.targetUserHost !== null, - }) - .then(); - } - - return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) }) - .then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved')); - } -} diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index b6b591d240..d4dbdbc485 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -20,6 +20,7 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { CacheService } from '@/core/CacheService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -59,6 +60,7 @@ export class AccountMoveService { private instanceChart: InstanceChart, private metaService: MetaService, private relayService: RelayService, + private cacheService: CacheService, private queueService: QueueService, ) { } @@ -82,7 +84,7 @@ export class AccountMoveService { Object.assign(src, update); // Update cache - this.globalEventService.publishInternalEvent('localUserUpdated', src); + this.cacheService.uriPersonCache.set(srcUri, src); const srcPerson = await this.apRendererService.renderPerson(src); const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src)); @@ -94,7 +96,7 @@ export class AccountMoveService { await this.apDeliverManagerService.deliverToFollowers(src, moveAct); // Publish meUpdated event - const iObj = await this.userEntityService.pack(src.id, src, { schema: 'MeDetailed', includeSecrets: true }); + const iObj = await this.userEntityService.pack(src.id, src, { detail: true, includeSecrets: true }); this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj); // Unfollow after 24 hours @@ -305,7 +307,7 @@ export class AccountMoveService { let resultUser: MiLocalUser | MiRemoteUser | null = null; if (this.userEntityService.isRemoteUser(dst)) { - if (Date.now() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { + if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { await this.apPersonService.updatePerson(dst.uri); } dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst; @@ -321,7 +323,7 @@ export class AccountMoveService { if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー if (this.userEntityService.isRemoteUser(dst)) { - if (Date.now() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { + if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { await this.apPersonService.updatePerson(srcUri); } diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 69a57b4854..339180854d 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 2b08b383a7..2b1504e51f 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -84,13 +84,10 @@ export const ACHIEVEMENT_TYPES = [ 'justPlainLucky', 'setNameToSyuilo', 'setNameToNoriDev', - 'setNameToYojo', 'cookieClicked', 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', - 'bubbleGameExplodingHead', - 'bubbleGameDoubleExplodingHead', ] as const; @Injectable() diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index ad852fdd6e..0da54dd8df 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index 4d8f11e595..df21be21b7 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -1,17 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; -import { Brackets, EntityNotFoundError } from 'typeorm'; +import { Brackets } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiUser } from '@/models/User.js'; import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { Packed } from '@/misc/json-schema.js'; import { IdService } from '@/core/IdService.js'; -import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -30,7 +29,6 @@ export class AnnouncementService { private idService: IdService, private globalEventService: GlobalEventService, private moderationLogService: ModerationLogService, - private announcementEntityService: AnnouncementEntityService, ) { } @@ -67,7 +65,7 @@ export class AnnouncementService { @bindThis public async create(values: Partial, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> { - const announcement = await this.announcementsRepository.insertOne({ + const announcement = await this.announcementsRepository.insert({ id: this.idService.gen(), updatedAt: null, title: values.title, @@ -79,9 +77,9 @@ export class AnnouncementService { silence: values.silence, needConfirmationToRead: values.needConfirmationToRead, userId: values.userId, - }); + }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); - const packed = await this.announcementEntityService.pack(announcement); + const packed = (await this.packMany([announcement]))[0]; if (announcement.isActive) { if (values.userId) { @@ -181,24 +179,6 @@ export class AnnouncementService { } } - @bindThis - public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise> { - const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId }); - if (me) { - if (announcement.userId && announcement.userId !== me.id) { - throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId }); - } - - const read = await this.announcementReadsRepository.findOneBy({ - announcementId: announcement.id, - userId: me.id, - }); - return this.announcementEntityService.pack({ ...announcement, isRead: read !== null }, me); - } else { - return this.announcementEntityService.pack(announcement, null); - } - } - @bindThis public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise { try { @@ -215,4 +195,29 @@ export class AnnouncementService { this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements'); } } + + @bindThis + public async packMany( + announcements: MiAnnouncement[], + me?: { id: MiUser['id'] } | null | undefined, + options?: { + reads?: MiAnnouncementRead[]; + }, + ): Promise[]> { + const reads = me ? (options?.reads ?? await this.getReads(me.id)) : []; + return announcements.map(announcement => ({ + id: announcement.id, + createdAt: this.idService.parse(announcement.id).date.toISOString(), + updatedAt: announcement.updatedAt?.toISOString() ?? null, + text: announcement.text, + title: announcement.title, + imageUrl: announcement.imageUrl, + icon: announcement.icon, + display: announcement.display, + needConfirmationToRead: announcement.needConfirmationToRead, + silence: announcement.silence, + forYou: announcement.userId === me?.id, + isRead: reads.some(read => read.announcementId === announcement.id), + })); + } } diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index eda6940a91..e690e5afd1 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -17,7 +17,6 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; -import { deserializeAntenna } from './deserializeAntenna.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -59,14 +58,24 @@ export class AntennaService implements OnApplicationShutdown { const { type, body } = obj.message as GlobalEvents['internal']['payload']; switch (type) { case 'antennaCreated': - this.antennas.push(deserializeAntenna(body)); + this.antennas.push({ + ...body, + lastUsedAt: new Date(body.lastUsedAt), + }); break; case 'antennaUpdated': { const idx = this.antennas.findIndex(a => a.id === body.id); if (idx >= 0) { - this.antennas[idx] = deserializeAntenna(body); + this.antennas[idx] = { + ...body, + lastUsedAt: new Date(body.lastUsedAt), + }; } else { - this.antennas.push(deserializeAntenna(body)); + // サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり + this.antennas.push({ + ...body, + lastUsedAt: new Date(body.lastUsedAt), + }); } } break; @@ -80,7 +89,7 @@ export class AntennaService implements OnApplicationShutdown { } @bindThis - public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise { + public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise { const antennas = await this.getAntennas(); const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const))); const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna); @@ -98,12 +107,9 @@ export class AntennaService implements OnApplicationShutdown { // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている @bindThis - public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise { + public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise { if (note.visibility === 'specified') return false; if (note.visibility === 'followers') return false; - if (note.visibility === 'private') return false; - - if (antenna.excludeBots && noteUser.isBot) return false; if (antenna.localOnly && noteUser.host != null) return false; diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts index bd2749cb87..57c5986ded 100644 --- a/packages/backend/src/core/AppLockService.ts +++ b/packages/backend/src/core/AppLockService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts index 4858c17e9a..06849ede1a 100644 --- a/packages/backend/src/core/AvatarDecorationService.ts +++ b/packages/backend/src/core/AvatarDecorationService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -21,7 +21,6 @@ import type { Config } from '@/config.js'; @Injectable() export class AvatarDecorationService implements OnApplicationShutdown { public cache: MemorySingleCache; - public cacheWithRemote: MemorySingleCache; constructor( @Inject(DI.config) @@ -45,7 +44,6 @@ export class AvatarDecorationService implements OnApplicationShutdown { private httpRequestService: HttpRequestService, ) { this.cache = new MemorySingleCache(1000 * 60 * 30); - this.cacheWithRemote = new MemorySingleCache(1000 * 60 * 30); this.redisForSub.on('message', this.onMessage); } @@ -71,10 +69,10 @@ export class AvatarDecorationService implements OnApplicationShutdown { @bindThis public async create(options: Partial, moderator?: MiUser): Promise { - const created = await this.avatarDecorationsRepository.insertOne({ + const created = await this.avatarDecorationsRepository.insert({ id: this.idService.gen(), ...options, - }); + }).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('avatarDecorationCreated', created); @@ -139,15 +137,16 @@ export class AvatarDecorationService implements OnApplicationShutdown { }); const userData: any = await res.json(); - const userAvatarDecorations = userData.avatarDecorations ?? undefined; + const avatarDecorations = userData.avatarDecorations?.[0]; - if (!userAvatarDecorations || userAvatarDecorations.length === 0) { + if (!avatarDecorations) { const updates = {} as Partial; updates.avatarDecorations = []; await this.usersRepository.update({ id: user.id }, updates); return; } + const avatarDecorationId = avatarDecorations.id; const instanceHost = instance.host; const decorationApiUrl = `https://${instanceHost}/api/get-avatar-decorations`; const allRes = await this.httpRequestService.send(decorationApiUrl, { @@ -155,61 +154,46 @@ export class AvatarDecorationService implements OnApplicationShutdown { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); - const allDecorations: any = await allRes.json(); - const updates = {} as Partial; - updates.avatarDecorations = []; - - for (const avatarDecoration of userAvatarDecorations) { - let name; - let description; - const avatarDecorationId = avatarDecoration.id; - - for (const decoration of allDecorations) { - // eslint-disable-next-line eqeqeq - if (decoration.id == avatarDecorationId) { - name = decoration.name; - description = decoration.description; - break; - } - } - - const existingDecoration = await this.avatarDecorationsRepository.findOneBy({ - host: userHost, - remoteId: avatarDecorationId, - }); - - const decorationData = { - name: name, - description: description, - url: this.getProxiedUrl(avatarDecoration.url, 'static'), - remoteId: avatarDecorationId, - host: userHost, - }; - - if (existingDecoration == null) { - await this.create(decorationData); - this.cacheWithRemote.delete(); - } else { - await this.update(existingDecoration.id, decorationData); - this.cacheWithRemote.delete(); + let name; + let description; + for (const decoration of allDecorations) { + if (decoration.id === avatarDecorationId) { + name = decoration.name; + description = decoration.description; + break; } - - const findDecoration = await this.avatarDecorationsRepository.findOneBy({ - host: userHost, - remoteId: avatarDecorationId, - }); - - updates.avatarDecorations.push({ - id: findDecoration?.id ?? '', - angle: avatarDecoration.angle ?? 0, - flipH: avatarDecoration.flipH ?? false, - offsetX: avatarDecoration.offsetX ?? 0, - offsetY: avatarDecoration.offsetY ?? 0, - scale: avatarDecoration.scale ?? 1, - opacity: avatarDecoration.opacity ?? 1, - }); } + const existingDecoration = await this.avatarDecorationsRepository.findOneBy({ + host: userHost, + remoteId: avatarDecorationId, + }); + const decorationData = { + name: name, + description: description, + url: this.getProxiedUrl(avatarDecorations.url, 'static'), + remoteId: avatarDecorationId, + host: userHost, + }; + if (existingDecoration == null) { + await this.create(decorationData); + } else { + await this.update(existingDecoration.id, decorationData); + } + const findDecoration = await this.avatarDecorationsRepository.findOneBy({ + host: userHost, + remoteId: avatarDecorationId, + }); + const updates = {} as Partial; + updates.avatarDecorations = [{ + id: findDecoration?.id ?? '', + angle: avatarDecorations.angle ?? 0, + flipH: avatarDecorations.flipH ?? false, + offsetX: avatarDecorations.offsetX ?? 0, + offsetY: avatarDecorations.offsetY ?? 0, + scale: avatarDecorations.scale ?? 1, + opacity: avatarDecorations.opacity ?? 1, + }]; await this.usersRepository.update({ id: user.id }, updates); } @@ -236,7 +220,7 @@ export class AvatarDecorationService implements OnApplicationShutdown { if (!withRemote) { return this.cache.fetch(() => this.avatarDecorationsRepository.find({ where: { host: IsNull() } })); } else { - return this.cacheWithRemote.fetch(() => this.avatarDecorationsRepository.find()); + return this.cache.fetch(() => this.avatarDecorationsRepository.find()); } } diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 9d6074f929..82fa1ddcc2 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -17,10 +17,10 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class CacheService implements OnApplicationShutdown { - public userByIdCache: MemoryKVCache; - public localUserByNativeTokenCache: MemoryKVCache; + public userByIdCache: MemoryKVCache; + public localUserByNativeTokenCache: MemoryKVCache; public localUserByIdCache: MemoryKVCache; - public uriPersonCache: MemoryKVCache; + public uriPersonCache: MemoryKVCache; public userProfileCache: RedisKVCache; public flashAccessTokensCache: RedisKVCache; public userMutingsCache: RedisKVCache>; @@ -58,10 +58,41 @@ export class CacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new MemoryKVCache(Infinity); - this.localUserByNativeTokenCache = new MemoryKVCache(Infinity); - this.localUserByIdCache = new MemoryKVCache(Infinity); - this.uriPersonCache = new MemoryKVCache(Infinity); + const localUserByIdCache = new MemoryKVCache(1000 * 60 * 60 * 6 /* 6h */); + this.localUserByIdCache = localUserByIdCache; + + // ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する + const userByIdCache = new MemoryKVCache(1000 * 60 * 60 * 6 /* 6h */, { + toMapConverter: user => { + if (user.host === null) { + localUserByIdCache.set(user.id, user as MiLocalUser); + return user.id; + } + + return user; + }, + fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId, + }); + this.userByIdCache = userByIdCache; + + this.localUserByNativeTokenCache = new MemoryKVCache(Infinity, { + toMapConverter: user => { + if (user === null) return null; + + localUserByIdCache.set(user.id, user); + return user.id; + }, + fromMapConverter: id => id === null ? null : localUserByIdCache.get(id), + }); + this.uriPersonCache = new MemoryKVCache(Infinity, { + toMapConverter: user => { + if (user === null) return null; + + userByIdCache.set(user.id, user); + return user.id; + }, + fromMapConverter: id => id === null ? null : userByIdCache.get(id), + }); this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', { lifetime: 1000 * 60 * 30, // 30m @@ -137,30 +168,18 @@ export class CacheService implements OnApplicationShutdown { const { type, body } = obj.message as GlobalEvents['internal']['payload']; switch (type) { case 'userChangeSuspendedState': - case 'userChangeDeletedState': - case 'remoteUserUpdated': - case 'localUserUpdated': { - const user = await this.usersRepository.findOneBy({ id: body.id }); - if (user == null) { - this.userByIdCache.delete(body.id); - this.localUserByIdCache.delete(body.id); - for (const [k, v] of this.uriPersonCache.cache.entries()) { - if (v.value?.id === body.id) { - this.uriPersonCache.delete(k); - } - } - } else { - this.userByIdCache.set(user.id, user); - for (const [k, v] of this.uriPersonCache.cache.entries()) { - if (v.value?.id === user.id) { - this.uriPersonCache.set(k, user); - } - } - if (this.userEntityService.isLocalUser(user)) { - this.localUserByNativeTokenCache.set(user.token!, user); - this.localUserByIdCache.set(user.id, user); + case 'remoteUserUpdated': { + const user = await this.usersRepository.findOneByOrFail({ id: body.id }); + this.userByIdCache.set(user.id, user); + for (const [k, v] of this.uriPersonCache.cache.entries()) { + if (v.value === user.id) { + this.uriPersonCache.set(k, user); } } + if (this.userEntityService.isLocalUser(user)) { + this.localUserByNativeTokenCache.set(user.token!, user); + this.localUserByIdCache.set(user.id, user); + } break; } case 'userTokenRegenerated': { diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index f6b7955cd2..c5c55af269 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -73,37 +73,6 @@ export class CaptchaService { } } - // https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go - @bindThis - public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise { - if (response == null) { - throw new Error('mcaptcha-failed: no response provided'); - } - - const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost); - const result = await this.httpRequestService.send(endpointUrl.toString(), { - method: 'POST', - body: JSON.stringify({ - key: siteKey, - secret: secret, - token: response, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (result.status !== 200) { - throw new Error('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'); - } - } - @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise { if (response == null) { diff --git a/packages/backend/src/core/ChannelFollowingService.ts b/packages/backend/src/core/ChannelFollowingService.ts index 12251595e2..75843b9773 100644 --- a/packages/backend/src/core/ChannelFollowingService.ts +++ b/packages/backend/src/core/ChannelFollowingService.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts index 929a9db064..8fa371346b 100644 --- a/packages/backend/src/core/ClipService.ts +++ b/packages/backend/src/core/ClipService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -41,17 +41,17 @@ export class ClipService { const currentCount = await this.clipsRepository.countBy({ userId: me.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) { + if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { throw new ClipService.TooManyClipsError(); } - const clip = await this.clipsRepository.insertOne({ + const clip = await this.clipsRepository.insert({ id: this.idService.gen(), userId: me.id, name: name, isPublic: isPublic, description: description, - }); + }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0])); return clip; } @@ -102,7 +102,7 @@ export class ClipService { const currentCount = await this.clipNotesRepository.countBy({ clipId: clip.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { + if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { throw new ClipService.TooManyClipNotesError(); } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 44f820fb0d..7332419584 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -1,18 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Module } from '@nestjs/common'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; -import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { UserSearchService } from '@/core/UserSearchService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; @@ -63,11 +55,10 @@ import { UserFollowingService } from './UserFollowingService.js'; import { UserKeypairService } from './UserKeypairService.js'; import { UserListService } from './UserListService.js'; import { UserMutingService } from './UserMutingService.js'; -import { UserRenoteMutingService } from './UserRenoteMutingService.js'; import { UserSuspendService } from './UserSuspendService.js'; import { UserAuthService } from './UserAuthService.js'; import { VideoProcessingService } from './VideoProcessingService.js'; -import { UserWebhookService } from './UserWebhookService.js'; +import { WebhookService } from './WebhookService.js'; import { ProxyAccountService } from './ProxyAccountService.js'; import { UtilityService } from './UtilityService.js'; import { FileInfoService } from './FileInfoService.js'; @@ -77,7 +68,6 @@ import { FeaturedService } from './FeaturedService.js'; import { FanoutTimelineService } from './FanoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; import { RegistryApiService } from './RegistryApiService.js'; - import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -92,9 +82,7 @@ import PerUserFollowingChart from './chart/charts/per-user-following.js'; import PerUserDriveChart from './chart/charts/per-user-drive.js'; import ApRequestChart from './chart/charts/ap-request.js'; import { ChartManagementService } from './chart/ChartManagementService.js'; - import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js'; -import { AnnouncementEntityService } from './entities/AnnouncementEntityService.js'; import { AntennaEntityService } from './entities/AntennaEntityService.js'; import { AppEntityService } from './entities/AppEntityService.js'; import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js'; @@ -129,8 +117,6 @@ import { UserListEntityService } from './entities/UserListEntityService.js'; import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; import { RoleEntityService } from './entities/RoleEntityService.js'; -import { MetaEntityService } from './entities/MetaEntityService.js'; - import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @@ -140,7 +126,7 @@ import { ApMfmService } from './activitypub/ApMfmService.js'; import { ApRendererService } from './activitypub/ApRendererService.js'; import { ApRequestService } from './activitypub/ApRequestService.js'; import { ApResolverService } from './activitypub/ApResolverService.js'; -import { JsonLdService } from './activitypub/JsonLdService.js'; +import { LdSignatureService } from './activitypub/LdSignatureService.js'; import { RemoteLoggerService } from './RemoteLoggerService.js'; import { RemoteUserResolveService } from './RemoteUserResolveService.js'; import { WebfingerService } from './WebfingerService.js'; @@ -157,8 +143,6 @@ import type { Provider } from '@nestjs/common'; //#region 文字列ベースでのinjection用(循環参照対応のため) const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService }; -const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService }; -const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService }; const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService }; const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService }; const $AiService: Provider = { provide: 'AiService', useExisting: AiService }; @@ -210,13 +194,10 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService }; -const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService }; -const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService }; const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService }; -const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService }; -const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService }; +const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; @@ -243,8 +224,6 @@ const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRe const $ChartManagementService: Provider = { provide: 'ChartManagementService', useExisting: ChartManagementService }; const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService }; -const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService }; -const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService }; const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService }; const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService }; const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService }; @@ -279,8 +258,6 @@ const $UserListEntityService: Provider = { provide: 'UserListEntityService', use const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; -const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService }; -const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -291,7 +268,7 @@ const $ApMfmService: Provider = { provide: 'ApMfmService', useExisting: ApMfmSer const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService }; const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService }; const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService }; -const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService }; +const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService }; const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService }; const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService }; const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService }; @@ -309,8 +286,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv ], providers: [ LoggerService, - AbuseReportService, - AbuseReportNotificationService, AccountMoveService, AccountUpdateService, AiService, @@ -362,13 +337,10 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv UserKeypairService, UserListService, UserMutingService, - UserRenoteMutingService, - UserSearchService, UserSuspendService, UserAuthService, VideoProcessingService, - UserWebhookService, - SystemWebhookService, + WebhookService, UtilityService, FileInfoService, SearchService, @@ -378,7 +350,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, - ChartLoggerService, FederationChart, NotesChart, @@ -393,10 +364,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv PerUserDriveChart, ApRequestChart, ChartManagementService, - AbuseUserReportEntityService, - AnnouncementEntityService, - AbuseReportNotificationRecipientEntityService, AntennaEntityService, AppEntityService, AuthSessionEntityService, @@ -431,9 +399,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv FlashEntityService, FlashLikeEntityService, RoleEntityService, - MetaEntityService, - SystemWebhookEntityService, - ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -443,7 +408,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv ApRendererService, ApRequestService, ApResolverService, - JsonLdService, + LdSignatureService, RemoteLoggerService, RemoteUserResolveService, WebfingerService, @@ -457,8 +422,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv //#region 文字列ベースでのinjection用(循環参照対応のため) $LoggerService, - $AbuseReportService, - $AbuseReportNotificationService, $AccountMoveService, $AccountUpdateService, $AiService, @@ -510,13 +473,10 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $UserKeypairService, $UserListService, $UserMutingService, - $UserRenoteMutingService, - $UserSearchService, $UserSuspendService, $UserAuthService, $VideoProcessingService, - $UserWebhookService, - $SystemWebhookService, + $WebhookService, $UtilityService, $FileInfoService, $SearchService, @@ -526,7 +486,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, - $ChartLoggerService, $FederationChart, $NotesChart, @@ -541,10 +500,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $PerUserDriveChart, $ApRequestChart, $ChartManagementService, - $AbuseUserReportEntityService, - $AnnouncementEntityService, - $AbuseReportNotificationRecipientEntityService, $AntennaEntityService, $AppEntityService, $AuthSessionEntityService, @@ -579,9 +535,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, - $MetaEntityService, - $SystemWebhookEntityService, - $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -591,7 +544,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $ApRendererService, $ApRequestService, $ApResolverService, - $JsonLdService, + $LdSignatureService, $RemoteLoggerService, $RemoteUserResolveService, $WebfingerService, @@ -606,8 +559,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv exports: [ QueueModule, LoggerService, - AbuseReportService, - AbuseReportNotificationService, AccountMoveService, AccountUpdateService, AiService, @@ -659,13 +610,10 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv UserKeypairService, UserListService, UserMutingService, - UserRenoteMutingService, - UserSearchService, UserSuspendService, UserAuthService, VideoProcessingService, - UserWebhookService, - SystemWebhookService, + WebhookService, UtilityService, FileInfoService, SearchService, @@ -675,7 +623,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, - FederationChart, NotesChart, UsersChart, @@ -689,10 +636,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv PerUserDriveChart, ApRequestChart, ChartManagementService, - AbuseUserReportEntityService, - AnnouncementEntityService, - AbuseReportNotificationRecipientEntityService, AntennaEntityService, AppEntityService, AuthSessionEntityService, @@ -727,9 +671,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv FlashEntityService, FlashLikeEntityService, RoleEntityService, - MetaEntityService, - SystemWebhookEntityService, - ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -739,7 +680,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv ApRendererService, ApRequestService, ApResolverService, - JsonLdService, + LdSignatureService, RemoteLoggerService, RemoteUserResolveService, WebfingerService, @@ -753,8 +694,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv //#region 文字列ベースでのinjection用(循環参照対応のため) $LoggerService, - $AbuseReportService, - $AbuseReportNotificationService, $AccountMoveService, $AccountUpdateService, $AiService, @@ -806,13 +745,10 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $UserKeypairService, $UserListService, $UserMutingService, - $UserRenoteMutingService, - $UserSearchService, $UserSuspendService, $UserAuthService, $VideoProcessingService, - $UserWebhookService, - $SystemWebhookService, + $WebhookService, $UtilityService, $FileInfoService, $SearchService, @@ -822,7 +758,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, - $FederationChart, $NotesChart, $UsersChart, @@ -836,10 +771,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $PerUserDriveChart, $ApRequestChart, $ChartManagementService, - $AbuseUserReportEntityService, - $AnnouncementEntityService, - $AbuseReportNotificationRecipientEntityService, $AntennaEntityService, $AppEntityService, $AuthSessionEntityService, @@ -874,9 +806,6 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, - $MetaEntityService, - $SystemWebhookEntityService, - $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -886,7 +815,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $ApRendererService, $ApRequestService, $ApResolverService, - $JsonLdService, + $LdSignatureService, $RemoteLoggerService, $RemoteUserResolveService, $WebfingerService, diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index e38c7b39cb..684b420453 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import { hashPassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { IsNull, DataSource } from 'typeorm'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { MiUser } from '@/models/User.js'; @@ -32,7 +32,8 @@ export class CreateSystemUserService { const password = randomUUID(); // Generate hash of password - const hash = await hashPassword(password); + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); // Generate secret const secret = generateNativeUserToken(); diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index cb1c048db6..1d90f0048c 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -20,7 +20,7 @@ import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/; +const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @Injectable() export class CustomEmojiService implements OnApplicationShutdown { @@ -68,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown { localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; }, moderator?: MiUser): Promise { - const emoji = await this.emojisRepository.insertOne({ + const emoji = await this.emojisRepository.insert({ id: this.idService.gen(), updatedAt: new Date(), name: data.name, @@ -82,7 +82,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, - }); + }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { this.localEmojisCache.refresh(); @@ -349,11 +349,10 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise> { const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost))); - const res = {} as Record; + const res = {} as any; for (let i = 0; i < emojiNames.length; i++) { - const resolvedEmoji = emojis[i]; - if (resolvedEmoji != null) { - res[emojiNames[i]] = resolvedEmoji; + if (emojis[i] != null) { + res[emojiNames[i]] = emojis[i]; } } return res; @@ -389,7 +388,7 @@ export class CustomEmojiService implements OnApplicationShutdown { */ @bindThis public checkDuplicate(name: string): Promise { - return this.emojisRepository.exists({ where: { name, host: IsNull() } }); + return this.emojisRepository.exist({ where: { name, host: IsNull() } }); } @bindThis @@ -397,11 +396,6 @@ export class CustomEmojiService implements OnApplicationShutdown { return this.emojisRepository.findOneBy({ id }); } - @bindThis - public getEmojiByName(name: string): Promise { - return this.emojisRepository.findOneBy({ name, host: IsNull() }); - } - @bindThis public dispose(): void { this.cache.dispose(); diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 79b614edba..e95655965f 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -9,7 +9,6 @@ import { QueueService } from '@/core/QueueService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; @Injectable() export class DeleteAccountService { @@ -19,7 +18,6 @@ export class DeleteAccountService { private userSuspendService: UserSuspendService, private queueService: QueueService, - private globalEventService: GlobalEventService, ) { } @@ -41,7 +39,5 @@ export class DeleteAccountService { await this.usersRepository.update(user.id, { isDeleted: true, }); - - this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true }); } } diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 21ae798f9f..1549cc4f3e 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -145,8 +145,7 @@ export class DownloadService { const parsedIp = ipaddr.parse(ip); for (const net of this.config.allowedPrivateNetworks ?? []) { - const cidr = ipaddr.parseCIDR(net); - if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { + if (parsedIp.match(ipaddr.parseCIDR(net))) { return false; } } diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index c8c918fa58..aa7e135469 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import sharp from 'sharp'; -import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; +import { sharpBmp } from 'sharp-read-bmp'; import { IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; @@ -43,8 +43,6 @@ import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { RegistryApiService } from '@/core/RegistryApiService.js'; -import { UtilityService } from '@/core/UtilityService.js'; type AddFileArgs = { /** User who wish to add file */ @@ -129,8 +127,6 @@ export class DriveService { private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, - private registryApiService: RegistryApiService, - private utilityService: UtilityService, ) { const logger = new Logger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); @@ -150,10 +146,8 @@ export class DriveService { @bindThis private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number, isRemote: boolean): Promise { // thunbnail, webpublic を必要なら生成 - const alts = file.userId == null ? { - webpublic: null, - thumbnail: null, - } : await this.generateAlts(path, type, !file.uri, file.userId); + const alts = await this.generateAlts(path, type, !file.uri); + const meta = await this.metaService.fetch(); if (meta.useObjectStorage) { @@ -235,7 +229,7 @@ export class DriveService { file.size = size; file.storedInternal = false; - return await this.driveFilesRepository.insertOne(file); + return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); } else { // use internal storage const accessKey = randomUUID(); const thumbnailAccessKey = 'thumbnail-' + randomUUID(); @@ -269,7 +263,7 @@ export class DriveService { file.md5 = hash; file.size = size; - return await this.driveFilesRepository.insertOne(file); + return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); } } @@ -280,7 +274,7 @@ export class DriveService { * @param generateWeb Generate webpublic or not */ @bindThis - public async generateAlts(path: string, type: string, generateWeb: boolean, userId: string) { + public async generateAlts(path: string, type: string, generateWeb: boolean) { if (type.startsWith('video/')) { if (this.config.videoThumbnailGenerator != null) { // videoThumbnailGeneratorが指定されていたら動画サムネイル生成はスキップ @@ -316,8 +310,7 @@ export class DriveService { let img: sharp.Sharp | null = null; let satisfyWebpublic: boolean; let isAnimated: boolean; - let width: number; - let height: number; + try { img = await sharpBmp(path, type); const metadata = await img.metadata(); @@ -330,8 +323,6 @@ export class DriveService { metadata.width && metadata.width <= 2048 && metadata.height && metadata.height <= 2048 ); - width = Number(metadata.width); - height = Number(metadata.height); } catch (err) { this.registerLogger.warn(`sharp failed: ${err}`); return { @@ -347,36 +338,10 @@ export class DriveService { this.registerLogger.info('creating web image'); try { - if (userId == null) { - width = 2048; - height = 2048; - } else { - const compressMode = await this.registryApiService.getItem(userId, null, ['client', 'base'], 'imageCompressionMode'); - this.registerLogger.debug(compressMode?.value); - switch (compressMode?.value) { - case 'resizeCompress': - width = 2048; - height = 2048; - break; - case 'noResizeCompress': - break; - case 'resizeCompressLossy': - width = 2048; - height = 2048; - break; - case 'noResizeCompressLossy': - break; - default: - this.registerLogger.debug('undefined CompressMode'); - width = 2048; - height = 2048; - break; - } - } if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) { - webpublic = await this.imageProcessingService.convertSharpToWebp(img, width, height); + webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048); } else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) { - webpublic = await this.imageProcessingService.convertSharpToPng(img, width, height); + webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); } else { this.registerLogger.debug('web image not created (not an required image)'); } @@ -528,14 +493,6 @@ export class DriveService { sensitiveThresholdForPorn: 0.75, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, }); - //ファイル単位の容量制限チェック - if (user == null) { - //system user skip - } else if (user.host !== null) { - //remote user skip - } else if (info.size > (await this.roleService.getUserPolicies(user.id)).fileSizeLimit * 1024 * 1024) { - throw new IdentifiableError('e5989b6d-ae66-49ed-88af-516ded10ca0c', 'File size limit over'); - } this.registerLogger.info(`${JSON.stringify(info)}`); // 現状 false positive が多すぎて実用に耐えない @@ -553,20 +510,14 @@ export class DriveService { if (user && !force) { // Check if there is a file with the same hash - const matched = await this.driveFilesRepository.findOneBy({ + const much = await this.driveFilesRepository.findOneBy({ md5: info.md5, userId: user.id, }); - if (matched) { - this.registerLogger.info(`file with same hash is found: ${matched.id}`); - if (sensitive && !matched.isSensitive) { - // The file is federated as sensitive for this time, but was federated as non-sensitive before. - // Therefore, update the file to sensitive. - await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true }); - matched.isSensitive = true; - } - return matched; + if (much) { + this.registerLogger.info(`file with same hash is found: ${much.id}`); + return much; } } @@ -643,7 +594,6 @@ export class DriveService { sensitive ?? false : false; - if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true; @@ -672,7 +622,7 @@ export class DriveService { file.type = info.type.mime; file.storedInternal = false; - file = await this.driveFilesRepository.insertOne(file); + file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); } catch (err) { // duplicate key error (when already registered) if (isDuplicateKeyValueError(err)) { @@ -719,7 +669,7 @@ export class DriveService { public async updateFile(file: MiDriveFile, values: Partial, updater: MiUser) { const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw; - if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) { + if (values.name && !this.driveFileEntityService.validateFileName(file.name)) { throw new DriveService.InvalidFileNameError(); } @@ -936,16 +886,4 @@ export class DriveService { cleanup(); } } - - @bindThis - public async getSensitiveFileCount(FileIds: string[]): Promise { - let SensitiveCount = 0; - - for (const FileId of FileIds) { - const file = await this.driveFilesRepository.findOneBy({ id: FileId }); - if (file?.isSensitive) SensitiveCount++; - } - - return SensitiveCount; - } } diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 508eabf25a..57823c5220 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,7 +15,6 @@ import type { UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { QueueService } from '@/core/QueueService.js'; @Injectable() export class EmailService { @@ -32,7 +31,6 @@ export class EmailService { private loggerService: LoggerService, private utilityService: UtilityService, private httpRequestService: HttpRequestService, - private queueService: QueueService, ) { this.logger = this.loggerService.getLogger('email'); } @@ -41,8 +39,6 @@ export class EmailService { public async sendEmail(to: string, subject: string, html: string, text: string) { const meta = await this.metaService.fetch(true); - if (!meta.enableEmail) return; - const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${this.config.url}/settings/email`; @@ -159,7 +155,7 @@ export class EmailService { @bindThis public async validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; - reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; + reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned'; }> { const meta = await this.metaService.fetch(); @@ -168,23 +164,14 @@ export class EmailService { email: emailAddress, }); - if (exist !== 0) { - return { - available: false, - reason: 'used', - }; - } - let validated: { valid: boolean, reason?: string | null, - } = { valid: true, reason: null }; + }; if (meta.enableActiveEmailValidation) { if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) { validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); - } else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) { - validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey); } else { validated = await validateEmail({ email: emailAddress, @@ -195,37 +182,25 @@ export class EmailService { validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので }); } - } - - if (!validated.valid) { - const formatReason: Record = { - regex: 'format', - disposable: 'disposable', - mx: 'mx', - smtp: 'smtp', - network: 'network', - blacklist: 'blacklist', - }; - - return { - available: false, - reason: validated.reason ? formatReason[validated.reason] ?? null : null, - }; + } else { + validated = { valid: true, reason: null }; } const emailDomain: string = emailAddress.split('@')[1]; const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain); - if (isBanned) { - return { - available: false, - reason: 'banned', - }; - } + const available = exist === 0 && validated.valid && !isBanned; return { - available: true, - reason: null, + available, + reason: available ? null : + exist !== 0 ? 'used' : + isBanned ? 'banned' : + validated.reason === 'regex' ? 'format' : + validated.reason === 'disposable' ? 'disposable' : + validated.reason === 'mx' ? 'mx' : + validated.reason === 'smtp' ? 'smtp' : + null, }; } @@ -242,8 +217,7 @@ export class EmailService { }, }); - const json = (await res.json()) as Partial<{ - message: string; + const json = (await res.json()) as { block: boolean; catch_all: boolean; deliverable_email: boolean; @@ -258,15 +232,8 @@ export class EmailService { mx_priority: { [key: string]: number }; privacy: boolean; related_domains: string[]; - }>; + }; - /* api error: when there is only one `message` attribute in the returned result */ - if (Object.keys(json).length === 1 && Reflect.has(json, 'message')) { - return { - valid: false, - reason: null, - }; - } if (json.email_address === undefined) { return { valid: false, @@ -297,68 +264,4 @@ export class EmailService { reason: null, }; } - - private async trueMail(truemailInstance: string, emailAddress: string, truemailAuthKey: string): Promise<{ - valid: boolean; - reason: 'used' | 'format' | 'blacklist' | 'mx' | 'smtp' | 'network' | T | null; - }> { - const endpoint = truemailInstance + '?email=' + emailAddress; - try { - const res = await this.httpRequestService.send(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: truemailAuthKey, - }, - }); - - const json = (await res.json()) as { - email: string; - success: boolean; - error?: string; - errors?: { - list_match?: string; - regex?: string; - mx?: string; - smtp?: string; - } | null; - }; - - if (json.email === undefined || json.errors?.regex) { - return { - valid: false, - reason: 'format', - }; - } - if (json.errors?.smtp) { - return { - valid: false, - reason: 'smtp', - }; - } - if (json.errors?.mx) { - return { - valid: false, - reason: 'mx', - }; - } - if (!json.success) { - return { - valid: false, - reason: json.errors?.list_match as T || 'blacklist', - }; - } - - return { - valid: true, - reason: null, - }; - } catch (error) { - return { - valid: false, - reason: 'network', - }; - } - } } diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index ad25b81b46..b25d058cc1 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,7 +13,7 @@ import type { NotesRepository } from '@/models/_.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { isPureRenote } from '@/misc/is-pure-renote.js'; import { CacheService } from '@/core/CacheService.js'; import { isReply } from '@/misc/is-reply.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; @@ -34,7 +34,6 @@ type TimelineOptions = { excludeReplies?: boolean; excludePureRenotes: boolean; withCats: boolean; - withoutBots: boolean; dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, }; @@ -57,20 +56,24 @@ export class FanoutTimelineEndpointService { @bindThis private async getMiNotes(ps: TimelineOptions): Promise { + let noteIds: string[]; + let shouldFallbackToDb = false; + // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); - const ascending = ps.sinceId && !ps.untilId; - const idCompare: (a: string, b: string) => number = ascending ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1; + const shouldPrepend = ps.sinceId && !ps.untilId; + const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1; const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); // TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい - const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare); + const redisResultIds = Array.from(new Set(redisResult.flat(1))); + + redisResultIds.sort(idCompare); + noteIds = redisResultIds.slice(0, ps.limit); - let noteIds = redisResultIds.slice(0, ps.limit); - const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1]; - const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId; + shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); if (!shouldFallbackToDb) { let filter = ps.noteFilter ?? (_note => true); @@ -93,7 +96,7 @@ export class FanoutTimelineEndpointService { if (ps.excludePureRenotes) { const parentFilter = filter; - filter = (note) => (!isRenote(note) || isQuote(note)) && parentFilter(note); + filter = (note) => !isPureRenote(note) && parentFilter(note); } if (ps.withCats) { @@ -101,11 +104,6 @@ export class FanoutTimelineEndpointService { filter = (note) => (note.user ? note.user.isCat : false) && parentFilter(note); } - if (ps.withoutBots) { - const parentFilter = filter; - filter = (note) => (!note.user || !note.user.isBot) && parentFilter(note); - } - if (ps.me) { const me = ps.me; const [ @@ -124,9 +122,8 @@ export class FanoutTimelineEndpointService { filter = (note) => { if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false; if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; - if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false; + if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false; if (isInstanceMuted(note, userMutedInstances)) return false; - if (!note.user || note.user.isSensitive) return false; return parentFilter(note); }; @@ -151,7 +148,9 @@ export class FanoutTimelineEndpointService { if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) { // 十分Redisからとれた - return redisTimeline.slice(0, ps.limit); + const result = redisTimeline.slice(0, ps.limit); + if (shouldPrepend) result.reverse(); + return result; } } @@ -159,7 +158,8 @@ export class FanoutTimelineEndpointService { const remainingToRead = ps.limit - redisTimeline.length; let dbUntil: string | null; let dbSince: string | null; - if (ascending) { + if (shouldPrepend) { + redisTimeline.reverse(); dbUntil = ps.untilId; dbSince = noteIds[noteIds.length - 1]; } else { @@ -167,7 +167,7 @@ export class FanoutTimelineEndpointService { dbSince = ps.sinceId; } const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead); - return [...redisTimeline, ...gotFromDb]; + return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb]; } return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index 199dc0ec5b..af528abf3e 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts index b3335e38da..ff45a601c7 100644 --- a/packages/backend/src/core/FeaturedService.ts +++ b/packages/backend/src/core/FeaturedService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index 7aeeb78178..d83194bd5c 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -40,7 +40,6 @@ export class FederatedInstanceService implements OnApplicationShutdown { firstRetrievedAt: new Date(parsed.firstRetrievedAt), latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null, infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null, - notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null, }; }, }); @@ -56,11 +55,11 @@ export class FederatedInstanceService implements OnApplicationShutdown { const index = await this.instancesRepository.findOneBy({ host }); if (index == null) { - const i = await this.instancesRepository.insertOne({ + const i = await this.instancesRepository.insert({ id: this.idService.gen(), host, firstRetrievedAt: new Date(), - }); + }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); this.federatedInstanceCache.set(host, i); return i; diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 3f3171327e..f56171490b 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -52,35 +52,21 @@ export class FetchInstanceMetadataService { } @bindThis - // public for test - public async tryLock(host: string): Promise { - // TODO: マイグレーションなのであとで消す (2024.3.1) - this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`); - - return await this.redisClient.set( - `fetchInstanceMetadata:mutex:v2:${host}`, '1', - 'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395 - 'GET', // 古い値を返す(なかったらnull) - ); + public async tryLock(host: string): Promise { + const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET'); + return mutex !== '1'; } @bindThis - // public for test - public unlock(host: string): Promise { - return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`); + public unlock(host: string): Promise<'OK'> { + return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0'); } @bindThis public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { const host = instance.host; - - // finallyでunlockされてしまうのでtry内でロックチェックをしない - // (returnであってもfinallyは実行される) - if (!force && await this.tryLock(host) === '1') { - // 1が返ってきていたらロックされているという意味なので、何もしない - return; - } - + // Acquire mutex to ensure no parallel runs + if (!await this.tryLock(host)) return; try { if (!force) { const _instance = await this.federatedInstanceService.fetch(host); @@ -155,7 +141,7 @@ export class FetchInstanceMetadataService { throw new Error('No wellknown links'); } - const links = wellknown.links as ({ rel: string, href: string; })[]; + const links = wellknown.links as any[]; const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index 169285f033..377d379170 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,12 +14,10 @@ import FFmpeg from 'fluent-ffmpeg'; import isSvg from 'is-svg'; import probeImageSize from 'probe-image-size'; import { type predictionType } from 'nsfwjs'; -import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; +import sharp from 'sharp'; import { encode } from 'blurhash'; import { createTempDir } from '@/misc/create-temp.js'; import { AiService } from '@/core/AiService.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; export type FileInfo = { @@ -50,13 +48,9 @@ const TYPE_SVG = { @Injectable() export class FileInfoService { - private logger: Logger; - constructor( private aiService: AiService, - private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('file-info'); } /** @@ -128,7 +122,7 @@ export class FileInfoService { 'image/avif', 'image/svg+xml', ].includes(type.mime)) { - blurhash = await this.getBlurhash(path, type.mime).catch(e => { + blurhash = await this.getBlurhash(path).catch(e => { warnings.push(`getBlurhash failed: ${e}`); return undefined; }); @@ -322,34 +316,6 @@ export class FileInfoService { return mime; } - /** - * ビデオファイルにビデオトラックがあるかどうかチェック - * (ない場合:m4a, webmなど) - * - * @param path ファイルパス - * @returns ビデオトラックがあるかどうか(エラー発生時は常に`true`を返す) - */ - @bindThis - private hasVideoTrackOnVideoFile(path: string): Promise { - const sublogger = this.logger.createSubLogger('ffprobe'); - sublogger.info(`Checking the video file. File path: ${path}`); - return new Promise((resolve) => { - try { - FFmpeg.ffprobe(path, (err, metadata) => { - if (err) { - sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err); - resolve(true); - return; - } - resolve(metadata.streams.some((stream) => stream.codec_type === 'video')); - }); - } catch (err) { - sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error); - resolve(true); - } - }); - } - /** * Detect MIME Type and extension */ @@ -372,20 +338,6 @@ export class FileInfoService { return TYPE_SVG; } - if ((type.mime.startsWith('video') || type.mime === 'application/ogg') && !(await this.hasVideoTrackOnVideoFile(path))) { - const newMime = `audio/${type.mime.split('/')[1]}`; - if (newMime === 'audio/mp4') { - return { - mime: 'audio/mp4', - ext: 'm4a', - }; - } - return { - mime: newMime, - ext: type.ext, - }; - } - return { mime: this.fixMime(type.mime), ext: type.ext, @@ -455,9 +407,9 @@ export class FileInfoService { * Calculate average color of image */ @bindThis - private getBlurhash(path: string, type: string): Promise { - return new Promise(async (resolve, reject) => { - (await sharpBmp(path, type)) + private getBlurhash(path: string): Promise { + return new Promise((resolve, reject) => { + sharp(path) .raw() .ensureAlpha() .resize(64, 64, { fit: 'inside' }) diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 3b8057e442..01a542c2cb 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -19,9 +19,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import type { MiSignin } from '@/models/Signin.js'; import type { MiPage } from '@/models/Page.js'; import type { MiWebhook } from '@/models/Webhook.js'; -import type { MiSystemWebhook } from '@/models/SystemWebhook.js'; import type { MiMeta } from '@/models/Meta.js'; -import type { MiNotification } from '@/models/Notification.js'; import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; @@ -57,23 +55,21 @@ export interface MainEventTypes { reply: Packed<'Note'>; renote: Packed<'Note'>; follow: Packed<'UserDetailedNotMe'>; - followed: Packed<'UserLite'>; - unfollow: Packed<'UserDetailedNotMe'>; - meUpdated: Packed<'MeDetailed'>; + followed: Packed<'User'>; + unfollow: Packed<'User'>; + meUpdated: Packed<'User'>; pageEvent: { pageId: MiPage['id']; event: string; var: any; userId: MiUser['id']; - user: Packed<'UserDetailed'>; + user: Packed<'User'>; }; urlUploadFinished: { marker?: string | null; file: Packed<'DriveFile'>; }; readAllNotifications: undefined; - notificationFlushed: undefined; - notificationDeleted: MiNotification['id']; unreadNotification: Packed<'Notification'>; unreadMention: MiNote['id']; readAllUnreadMentions: undefined; @@ -100,7 +96,7 @@ export interface MainEventTypes { }; driveFileCreated: Packed<'DriveFile'>; readAntenna: MiAntenna; - receiveFollowRequest: Packed<'UserLite'>; + receiveFollowRequest: Packed<'User'>; announcementCreated: { announcement: Packed<'Announcement'>; }; @@ -152,8 +148,8 @@ export interface ChannelEventTypes { } export interface UserListEventTypes { - userAdded: Packed<'UserLite'>; - userRemoved: Packed<'UserLite'>; + userAdded: Packed<'User'>; + userRemoved: Packed<'User'>; } export interface AntennaEventTypes { @@ -209,16 +205,10 @@ type SerializedAll = { [K in keyof T]: Serialized; }; -type UndefinedAsNullAll = { - [K in keyof T]: T[K] extends undefined ? null : T[K]; -} - export interface InternalEventTypes { userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; - userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; }; userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; remoteUserUpdated: { id: MiUser['id']; }; - localUserUpdated: { id: MiUser['id']; }; follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; @@ -232,9 +222,6 @@ export interface InternalEventTypes { webhookCreated: MiWebhook; webhookDeleted: MiWebhook; webhookUpdated: MiWebhook; - systemWebhookCreated: MiSystemWebhook; - systemWebhookDeleted: MiSystemWebhook; - systemWebhookUpdated: MiSystemWebhook; antennaCreated: MiAntenna; antennaDeleted: MiAntenna; antennaUpdated: MiAntenna; @@ -251,29 +238,27 @@ export interface InternalEventTypes { userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; }; } -type EventTypesToEventPayload = EventUnionFromDictionary>>; - // name/messages(spec) pairs dictionary export type GlobalEvents = { internal: { name: 'internal'; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; broadcast: { name: 'broadcast'; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; main: { name: `mainStream:${MiUser['id']}`; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; drive: { name: `driveStream:${MiUser['id']}`; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; note: { name: `noteStream:${MiNote['id']}`; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; channel: { name: `channelStream:${MiChannel['id']}`; @@ -281,7 +266,7 @@ export type GlobalEvents = { }; userList: { name: `userListStream:${MiUserList['id']}`; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; messaging: { name: `messagingStream:${MiUser['id']}-${MiUser['id']}`; @@ -297,15 +282,15 @@ export type GlobalEvents = { }; roleTimeline: { name: `roleTimelineStream:${MiRole['id']}`; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; antenna: { name: `antennaStream:${MiAntenna['id']}`; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; admin: { name: `adminStream:${MiUser['id']}`; - payload: EventTypesToEventPayload; + payload: EventUnionFromDictionary>; }; notes: { name: 'notesStream'; diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index eb192ee6da..c1c573c543 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -163,7 +163,7 @@ export class HashtagService { const instance = await this.metaService.fetch(); const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); if (hiddenTags.includes(hashtag)) return; - if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return; + if (this.utilityService.isSensitiveWordIncluded(hashtag, instance.sensitiveWords)) return; // YYYYMMDDHHmm (10分間隔) const now = new Date(); diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 3eb2a8089a..05d708aa5a 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,16 +14,9 @@ import { DI } from '@/di-symbols.js'; 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 type { IObject } from '@/core/activitypub/type.js'; import type { Response } from 'node-fetch'; import type { URL } from 'node:url'; -export type HttpRequestSendOptions = { - throwErrorWhenResponseNotOk: boolean; - validators?: ((res: Response) => void)[]; -}; - @Injectable() export class HttpRequestService { /** @@ -111,23 +104,6 @@ export class HttpRequestService { } } - @bindThis - public async getActivityJson(url: string): Promise { - const res = await this.send(url, { - method: 'GET', - headers: { - Accept: 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - }, - timeout: 5000, - size: 1024 * 256, - }, { - throwErrorWhenResponseNotOk: true, - validators: [validateContentTypeSetAsActivityPub], - }); - - return await res.json() as IObject; - } - @bindThis public async getJson(url: string, accept = 'application/json, */*', headers?: Record): Promise { const res = await this.send(url, { @@ -156,20 +132,17 @@ export class HttpRequestService { } @bindThis - public async send( - url: string, - args: { - method?: string, - body?: string, - headers?: Record, - timeout?: number, - size?: number, - } = {}, - extra: HttpRequestSendOptions = { - throwErrorWhenResponseNotOk: true, - validators: [], - }, - ): Promise { + public async send(url: string, args: { + method?: string, + body?: string, + headers?: Record, + timeout?: number, + size?: number, + } = {}, extra: { + throwErrorWhenResponseNotOk: boolean; + } = { + throwErrorWhenResponseNotOk: true, + }): Promise { const timeout = args.timeout ?? 5000; const controller = new AbortController(); @@ -196,12 +169,6 @@ export class HttpRequestService { throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); } - if (res.ok) { - for (const validator of (extra.validators ?? [])) { - validator(res); - } - } - return res; } diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index 10df6ef266..3fad220e49 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index 6f978b34c8..8b4aff5b35 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index 22c47297a3..5ecea91ea0 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, Not } from 'typeorm'; +import { IsNull } from 'typeorm'; import type { MiLocalUser } from '@/models/User.js'; import type { UsersRepository } from '@/models/_.js'; import { MemorySingleCache } from '@/misc/cache.js'; @@ -27,14 +27,6 @@ export class InstanceActorService { this.cache = new MemorySingleCache(Infinity); } - @bindThis - public async realLocalUsersPresent(): Promise { - return await this.usersRepository.existsBy({ - host: IsNull(), - username: Not(ACTOR_USERNAME), - }); - } - @bindThis public async getInstanceActor(): Promise { const cached = this.cache.get(); diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 4fb8a93e49..777d69e972 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index 4e5cf4f9da..2425c27be5 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -23,8 +23,8 @@ export class LoggerService { } @bindThis - public getLogger(domain: string, color?: KEYWORD | undefined) { + public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) { const logger = this.cloudLogging?.log(this.config.cloudLogging?.logName ?? 'cherrypick'); - return new Logger(domain, color, logger); + return new Logger(domain, color, store, logger); } } diff --git a/packages/backend/src/core/MessagingService.ts b/packages/backend/src/core/MessagingService.ts index d316cd611b..5891f2c8ea 100644 --- a/packages/backend/src/core/MessagingService.ts +++ b/packages/backend/src/core/MessagingService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -137,7 +137,7 @@ export class MessagingService { userId: message.userId, visibility: 'specified', emojis: [{}], - tags: [], + tags: [{}], mentions: [recipientUser].map(u => u.id), mentionedRemoteUsers: JSON.stringify([recipientUser].map(u => ({ uri: u.uri, @@ -146,7 +146,7 @@ export class MessagingService { host: u.host, } as IMentionedRemoteUsers[0] ))), - } as unknown as MiNote; + } as MiNote; const activity = this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note)); diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index ec630f804e..464c53e1f2 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -51,10 +51,7 @@ export class MetaService implements OnApplicationShutdown { const { type, body } = obj.message as GlobalEvents['internal']['payload']; switch (type) { case 'metaUpdated': { - this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - proxyAccount: null, // joinなカラムは通常取ってこないので - }; + this.cache = body; break; } default: diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 6d1674d1a9..722dbdfc99 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -1,24 +1,21 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; -import { Window, XMLSerializer } from 'happy-dom'; +import { Window } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; -import type { DefaultTreeAdapterMap } from 'parse5'; +import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; import type * as mfm from 'cherrypick-mfm-js'; -const treeAdapter = parse5.defaultTreeAdapter; -type Node = DefaultTreeAdapterMap['node']; -type ChildNode = DefaultTreeAdapterMap['childNode']; +const treeAdapter = TreeAdapter.defaultTreeAdapter; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; @@ -36,8 +33,6 @@ export class MfmService { // some AP servers like Pixelfed use br tags as well as newlines html = html.replace(/\r?\n/gi, '\n'); - const normalizedHashtagNames = hashtagNames == null ? undefined : new Set(hashtagNames.map(x => normalizeForSearch(x))); - const dom = parse5.parseFragment(html); let text = ''; @@ -48,7 +43,7 @@ export class MfmService { return text.trim(); - function getText(node: Node): string { + function getText(node: TreeAdapter.Node): string { if (treeAdapter.isTextNode(node)) return node.value; if (!treeAdapter.isElementNode(node)) return ''; if (node.nodeName === 'br') return '\n'; @@ -60,7 +55,7 @@ export class MfmService { return ''; } - function appendChildren(childNodes: ChildNode[]): void { + function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { if (childNodes) { for (const n of childNodes) { analyze(n); @@ -68,16 +63,14 @@ export class MfmService { } } - function analyze(node: Node) { + function analyze(node: TreeAdapter.Node) { if (treeAdapter.isTextNode(node)) { text += node.value; return; } // Skip comment or document type node - if (!treeAdapter.isElementNode(node)) { - return; - } + if (!treeAdapter.isElementNode(node)) return; switch (node.nodeName) { case 'br': { @@ -85,15 +78,16 @@ export class MfmService { break; } - case 'a': { + case 'a': + { const txt = getText(node); const rel = node.attrs.find(x => x.name === 'rel'); const href = node.attrs.find(x => x.name === 'href'); // ハッシュタグ - if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) { + if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { text += txt; - // メンション + // メンション } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { const part = txt.split('@'); @@ -105,7 +99,7 @@ export class MfmService { } else if (part.length === 3) { text += txt; } - // その他 + // その他 } else { const generateLink = () => { if (!href && !txt) { @@ -133,7 +127,8 @@ export class MfmService { break; } - case 'h1': { + case 'h1': + { text += '【'; appendChildren(node.childNodes); text += '】\n'; @@ -141,14 +136,16 @@ export class MfmService { } case 'b': - case 'strong': { + case 'strong': + { text += '**'; appendChildren(node.childNodes); text += '**'; break; } - case 'small': { + case 'small': + { text += ''; appendChildren(node.childNodes); text += ''; @@ -156,7 +153,8 @@ export class MfmService { } case 's': - case 'del': { + case 'del': + { text += '~~'; appendChildren(node.childNodes); text += '~~'; @@ -164,7 +162,8 @@ export class MfmService { } case 'i': - case 'em': { + case 'em': + { text += ''; appendChildren(node.childNodes); text += ''; @@ -205,7 +204,8 @@ export class MfmService { case 'h3': case 'h4': case 'h5': - case 'h6': { + case 'h6': + { text += '\n\n'; appendChildren(node.childNodes); break; @@ -218,7 +218,8 @@ export class MfmService { case 'article': case 'li': case 'dt': - case 'dd': { + case 'dd': + { text += '\n'; appendChildren(node.childNodes); break; @@ -243,8 +244,6 @@ export class MfmService { const doc = window.document; - const body = doc.createElement('p'); - function appendChildren(children: mfm.MfmNode[], targetElement: any): void { if (children) { for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child); @@ -420,10 +419,6 @@ export class MfmService { }, text: (node) => { - if (!node.props.text.match(/[\r\n]/)) { - return doc.createTextNode(node.props.text); - } - const el = doc.createElement('span'); const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); @@ -455,8 +450,8 @@ export class MfmService { }, }; - appendChildren(nodes, body); + appendChildren(nodes, doc.body); - return new XMLSerializer().serializeToString(body); + return `

${doc.body.innerHTML}

`; } } diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts index 6c155c9a62..5d2e52709c 100644 --- a/packages/backend/src/core/ModerationLogService.ts +++ b/packages/backend/src/core/ModerationLogService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 5490a449f4..35b161272d 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -40,7 +40,7 @@ import InstanceChart from '@/core/chart/charts/instance.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; -import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { WebhookService } from '@/core/WebhookService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { AntennaService } from '@/core/AntennaService.js'; import { QueueService } from '@/core/QueueService.js'; @@ -54,14 +54,12 @@ import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; +import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { isReply } from '@/misc/is-reply.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { Data } from 'ws'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -151,15 +149,12 @@ type Option = { uri?: string | null; url?: string | null; app?: MiApp | null; - deleteAt?: Date | null; }; @Injectable() export class NoteCreateService implements OnApplicationShutdown { #shutdownController = new AbortController(); - public static ContainsProhibitedWordsError = class extends Error {}; - constructor( @Inject(DI.config) private config: Config, @@ -212,13 +207,14 @@ export class NoteCreateService implements OnApplicationShutdown { private federatedInstanceService: FederatedInstanceService, private hashtagService: HashtagService, private antennaService: AntennaService, - private webhookService: UserWebhookService, + private webhookService: WebhookService, private featuredService: FeaturedService, private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, private metaService: MetaService, + private searchService: SearchService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, @@ -234,8 +230,6 @@ export class NoteCreateService implements OnApplicationShutdown { host: MiUser['host']; isBot: MiUser['isBot']; isCat: MiUser['isCat']; - isIndexable: MiUser['isIndexable']; - isSensitive: MiUser['isSensitive']; }, data: Option, silent = false): Promise { // チャンネル外にリプライしたら対象のスコープに合わせる // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) @@ -265,23 +259,13 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.visibility === 'public' && data.channel == null) { const sensitiveWords = meta.sensitiveWords; - if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { + if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; } } - const hasProhibitedWords = await this.checkProhibitedWordsContain({ - cw: data.cw, - text: data.text, - pollChoices: data.poll?.choices, - }, meta.prohibitedWords); - - if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); - } - const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host); if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { @@ -309,14 +293,13 @@ export class NoteCreateService implements OnApplicationShutdown { data.visibility = 'followers'; break; case 'specified': - case 'private': - // specified / direct note/ private noteはreject + // specified / direct noteはreject throw new Error('Renote target is not public or home'); } } // Check blocking - if (this.isRenote(data) && !this.isQuote(data)) { + if (data.renote && !this.isQuote(data)) { if (data.renote.userHost === null) { if (data.renote.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); @@ -347,9 +330,6 @@ export class NoteCreateService implements OnApplicationShutdown { data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); } data.text = data.text.trim(); - if (data.text === '') { - data.text = null; - } } else { data.text = null; } @@ -375,9 +355,6 @@ export class NoteCreateService implements OnApplicationShutdown { mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens); } - // if the host is media-silenced, custom emojis are not allowed - if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; - tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { @@ -398,10 +375,6 @@ export class NoteCreateService implements OnApplicationShutdown { } } - if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) { - throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions'); - } - const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); setImmediate('post created', { signal: this.#shutdownController.signal }).then( @@ -430,7 +403,6 @@ export class NoteCreateService implements OnApplicationShutdown { hasPoll: data.poll != null, hasEvent: data.event != null, cw: data.cw ?? null, - deleteAt: data.deleteAt, tags: tags.map(tag => normalizeForSearch(tag)), emojis, userId: user.id, @@ -490,7 +462,6 @@ export class NoteCreateService implements OnApplicationShutdown { noteVisibility: insert.visibility, userId: user.id, userHost: user.host, - channelId: insert.channelId, }); await transactionalEntityManager.insert(MiPoll, poll); @@ -510,10 +481,6 @@ export class NoteCreateService implements OnApplicationShutdown { await transactionalEntityManager.insert(MiEvent, event); } - - if (insert.visibility === 'private') { - insert.localOnly = true - } }); } else { await this.notesRepository.insert(insert); @@ -540,8 +507,6 @@ export class NoteCreateService implements OnApplicationShutdown { username: MiUser['username']; host: MiUser['host']; isBot: MiUser['isBot']; - isIndexable: MiUser['isIndexable']; - isSensitive: MiUser['isSensitive']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { const meta = await this.metaService.fetch(); @@ -607,16 +572,6 @@ export class NoteCreateService implements OnApplicationShutdown { }); } - if (data.deleteAt) { - const delay = data.deleteAt.getTime() - Date.now(); - this.queueService.scheduledNoteDeleteQueue.add(note.id, { - noteId: note.id, - }, { - delay, - removeOnComplete: true, - }); - } - if (!silent) { if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); @@ -655,7 +610,7 @@ export class NoteCreateService implements OnApplicationShutdown { 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', { + this.queueService.webhookDeliver(webhook, 'note', { note: noteObj, }); } @@ -669,7 +624,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.reply) { // 通知 if (data.reply.userHost === null) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ where: { userId: data.reply.userId, threadId: data.reply.threadId ?? data.reply.id, @@ -682,7 +637,7 @@ export class NoteCreateService implements OnApplicationShutdown { 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', { + this.queueService.webhookDeliver(webhook, 'reply', { note: noteObj, }); } @@ -691,7 +646,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // If it is renote - if (this.isRenote(data)) { + if (data.renote) { const type = this.isQuote(data) ? 'quote' : 'renote'; // Notify @@ -705,7 +660,7 @@ export class NoteCreateService implements OnApplicationShutdown { 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', { + this.queueService.webhookDeliver(webhook, 'renote', { note: noteObj, }); } @@ -746,7 +701,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.relayService.deliverToRelays(user, noteActivity); } - trackPromise(dm.execute()); + dm.execute(); })(); } //#endregion @@ -770,23 +725,14 @@ export class NoteCreateService implements OnApplicationShutdown { }); } + // Register to search database + this.index(note); } @bindThis - private isRenote(note: Option): note is Option & { renote: MiNote } { - return note.renote != null; - } - - @bindThis - private isQuote(note: Option & { renote: MiNote }): note is Option & { renote: MiNote } & ( - { text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] } - ) { - // NOTE: SYNC WITH misc/is-quote.ts - return note.text != null || - note.reply != null || - note.cw != null || - note.poll != null || - (note.files != null && note.files.length > 0); + private isQuote(note: Option): note is Option & { renote: MiNote } { + // sync with misc/is-quote.ts + return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll); } @bindThis @@ -816,7 +762,7 @@ export class NoteCreateService implements OnApplicationShutdown { @bindThis private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) { for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ where: { userId: u.id, threadId: note.threadId ?? note.id, @@ -835,7 +781,7 @@ export class NoteCreateService implements OnApplicationShutdown { 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', { + this.queueService.webhookDeliver(webhook, 'mention', { note: detailPackedNote, }); } @@ -854,13 +800,20 @@ export class NoteCreateService implements OnApplicationShutdown { private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { if (data.localOnly) return null; - const content = this.isRenote(data) && !this.isQuote(data) + const content = data.renote && !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); return this.apRendererService.addContext(content); } + @bindThis + private index(note: MiNote) { + if (note.text == null && note.cw == null) return; + + this.searchService.indexNote(note); + } + @bindThis private incNotesCountOfUser(user: { id: MiUser['id']; }) { this.usersRepository.createQueryBuilder().update() @@ -879,7 +832,7 @@ export class NoteCreateService implements OnApplicationShutdown { const mentions = extractMentions(tokens); let mentionedUsers = (await Promise.all(mentions.map(m => this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), - ))).filter(x => x != null); + ))).filter(x => x != null) as MiUser[]; // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => @@ -974,13 +927,10 @@ export class NoteCreateService implements OnApplicationShutdown { } } - // 自分自身のHTL - if (note.userHost == null) { - if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { - this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); - } + if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + if (note.fileIds.length > 0) { + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -1056,23 +1006,6 @@ export class NoteCreateService implements OnApplicationShutdown { } } - public async checkProhibitedWordsContain(content: Parameters[0], prohibitedWords?: string[]) { - if (prohibitedWords == null) { - prohibitedWords = (await this.metaService.fetch()).prohibitedWords; - } - - if ( - this.utilityService.isKeyWordIncluded( - this.utilityService.concatNoteContentsForKeyWordCheck(content), - prohibitedWords, - ) - ) { - return true; - } - - return false; - } - @bindThis public dispose(): void { this.#shutdownController.abort(); diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 56b2675da8..04250894e2 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -22,8 +22,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; +import { SearchService } from '@/core/SearchService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { isPureRenote } from '@/misc/is-pure-renote.js'; @Injectable() export class NoteDeleteService { @@ -48,6 +49,7 @@ export class NoteDeleteService { private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, private metaService: MetaService, + private searchService: SearchService, private moderationLogService: ModerationLogService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, @@ -61,7 +63,7 @@ export class NoteDeleteService { */ async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) { const deletedAt = new Date(); - // const cascadingNotes = await this.findCascadingNotes(note); + const cascadingNotes = await this.findCascadingNotes(note); if (note.replyId) { await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); @@ -77,7 +79,7 @@ export class NoteDeleteService { let renote: MiNote | null = null; // if deleted note is renote - if (isRenote(note) && !isQuote(note)) { + if (isPureRenote(note)) { renote = await this.notesRepository.findOneBy({ id: note.renoteId, }); @@ -90,7 +92,6 @@ export class NoteDeleteService { this.deliverToConcerned(user, note, content); } - /* // also deliever delete activity to cascaded notes const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes for (const cascadingNote of federatedLocalCascadingNotes) { @@ -99,7 +100,6 @@ export class NoteDeleteService { const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); this.deliverToConcerned(cascadingNote.user, cascadingNote, content); } - */ //#endregion const meta = await this.metaService.fetch(); @@ -119,6 +119,11 @@ export class NoteDeleteService { } } + for (const cascadingNote of cascadingNotes) { + this.searchService.unindexNote(cascadingNote); + } + this.searchService.unindexNote(note); + await this.notesRepository.delete({ id: note.id, userId: user.id, @@ -136,7 +141,6 @@ export class NoteDeleteService { } } - /* @bindThis private async findCascadingNotes(note: MiNote): Promise { const recursive = async (noteId: string): Promise => { @@ -159,7 +163,6 @@ export class NoteDeleteService { return cascadingNotes; } - */ @bindThis private async getMentionedRemoteUsers(note: MiNote) { diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index d38b48b65d..49ad5cf1aa 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index 181c9f7649..15f23f0090 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,7 +14,6 @@ import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; @Injectable() export class NoteReadService implements OnApplicationShutdown { @@ -49,7 +48,7 @@ export class NoteReadService implements OnApplicationShutdown { //#endregion // スレッドミュート - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ where: { userId: userId, threadId: note.threadId ?? note.id, @@ -70,7 +69,7 @@ export class NoteReadService implements OnApplicationShutdown { // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { - const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } }); + const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } }); if (!exist) return; @@ -88,47 +87,46 @@ export class NoteReadService implements OnApplicationShutdown { userId: MiUser['id'], notes: (MiNote | Packed<'Note'>)[], ): Promise { - if (notes.length === 0) return; - - const noteIds = new Set(); + const readMentions: (MiNote | Packed<'Note'>)[] = []; + const readSpecifiedNotes: (MiNote | Packed<'Note'>)[] = []; for (const note of notes) { if (note.mentions && note.mentions.includes(userId)) { - noteIds.add(note.id); + readMentions.push(note); } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { - noteIds.add(note.id); + readSpecifiedNotes.push(note); } } - if (noteIds.size === 0) return; - - // Remove the record - await this.noteUnreadsRepository.delete({ - userId: userId, - noteId: In(Array.from(noteIds)), - }); + if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) { + // Remove the record + await this.noteUnreadsRepository.delete({ + userId: userId, + noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), + }); - // TODO: ↓まとめてクエリしたい + // TODO: ↓まとめてクエリしたい - trackPromise(this.noteUnreadsRepository.countBy({ - userId: userId, - isMentioned: true, - }).then(mentionsCount => { - if (mentionsCount === 0) { - // 全て既読になったイベントを発行 - this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); - } - })); - - trackPromise(this.noteUnreadsRepository.countBy({ - userId: userId, - isSpecified: true, - }).then(specifiedCount => { - if (specifiedCount === 0) { - // 全て既読になったイベントを発行 - this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); - } - })); + this.noteUnreadsRepository.countBy({ + userId: userId, + isMentioned: true, + }).then(mentionsCount => { + if (mentionsCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); + } + }); + + this.noteUnreadsRepository.countBy({ + userId: userId, + isSpecified: true, + }).then(specifiedCount => { + if (specifiedCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + } + }); + } } @bindThis diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts index b4b2564e52..217c40aa46 100644 --- a/packages/backend/src/core/NoteUpdateService.ts +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -76,8 +76,6 @@ export class NoteUpdateService implements OnApplicationShutdown { username: MiUser['username']; host: MiUser['host']; isBot: MiUser['isBot']; - isIndexable: MiUser['isIndexable']; - isSensitive: MiUser['isSensitive']; }, data: Option, note: MiNote, silent = false): Promise { if (data.updatedAt == null) data.updatedAt = new Date(); @@ -213,8 +211,6 @@ export class NoteUpdateService implements OnApplicationShutdown { username: MiUser['username']; host: MiUser['host']; isBot: MiUser['isBot']; - isIndexable: MiUser['isIndexable']; - isSensitive: MiUser['isSensitive']; }, silent: boolean) { if (!silent) { if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); @@ -233,6 +229,9 @@ export class NoteUpdateService implements OnApplicationShutdown { } //#endregion } + + // Register to search database + this.reIndex(note); } @bindThis @@ -279,6 +278,14 @@ export class NoteUpdateService implements OnApplicationShutdown { } } + @bindThis + private reIndex(note: MiNote) { + if (note.text == null && note.cw == null) return; + + this.searchService.unindexNote(note); + this.searchService.indexNote(note); + } + @bindThis public dispose(): void { this.#shutdownController.abort(); diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 7cdcd38f62..41a9ce2d6e 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -20,7 +20,6 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { UserListService } from '@/core/UserListService.js'; import type { FilterUnionByProperty } from '@/types.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; @Injectable() export class NotificationService implements OnApplicationShutdown { @@ -75,18 +74,7 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - public createNotification( - notifieeId: MiUser['id'], - type: T, - data: Omit, 'type' | 'id' | 'createdAt' | 'notifierId'>, - notifierId?: MiUser['id'] | null, - ) { - trackPromise( - this.#createNotificationInternal(notifieeId, type, data, notifierId), - ); - } - - async #createNotificationInternal( + public async createNotification( notifieeId: MiUser['id'], type: T, data: Omit, 'type' | 'id' | 'createdAt' | 'notifierId'>, @@ -122,14 +110,6 @@ export class NotificationService implements OnApplicationShutdown { return null; } } else if (recieveConfig?.type === 'mutualFollow') { - const [isFollowing, isFollower] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), - this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), - ]); - if (!(isFollowing && isFollower)) { - return null; - } - } else if (recieveConfig?.type === 'followingOrFollower') { const [isFollowing, isFollower] = await Promise.all([ this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), @@ -163,8 +143,6 @@ export class NotificationService implements OnApplicationShutdown { const packed = await this.notificationEntityService.pack(notification, notifieeId, {}); - if (packed == null) return null; - // Publish notification event this.globalEventService.publishMainStream(notifieeId, 'notification', packed); @@ -214,35 +192,6 @@ export class NotificationService implements OnApplicationShutdown { */ } - @bindThis - public async flushAllNotifications(userId: MiUser['id']) { - await Promise.all([ - this.redisClient.del(`notificationTimeline:${userId}`), - this.redisClient.del(`latestReadNotification:${userId}`), - ]); - this.globalEventService.publishMainStream(userId, 'notificationFlushed'); - } - - async #getNotifications(userId: MiUser['id'], notificationId: MiNotification['id']) { - const notificationRes = await this.redisClient.xrange( - `notificationTimeline:${userId}`, - `${this.idService.parse(notificationId).date.getTime() - 1000}-0`, - `${this.idService.parse(notificationId).date.getTime() + 1000}-9999 `, - 'COUNT', 50 - ); - return notificationRes.find(x => JSON.parse(x[1][1]).id === notificationId); - } - - @bindThis - public async deleteNotification(userId: MiUser['id'], notificationId: MiNotification['id']) : Promise { - const targetResId = (await this.#getNotifications(userId, notificationId))?.[0]; - if (!targetResId) return; - - await this.redisClient.xdel(`notificationTimeline:${userId}`, targetResId); - this.globalEventService.publishMainStream(userId, 'notificationDeleted', notificationId); - return notificationId; - } - @bindThis public dispose(): void { this.#shutdownController.abort(); diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 6c96ab16cf..361a8d1375 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 71d663bf90..a163681f1c 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 239124b484..b926b47940 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -106,7 +106,7 @@ export class PushNotificationService implements OnApplicationShutdown { type, body: (type === 'notification' || type === 'unreadAntennaNote') ? truncateBody(type, body) : body, userId, - dateTime: Date.now(), + dateTime: (new Date()).getTime(), }), { proxy: this.config.proxy, }).catch((err: any) => { @@ -120,19 +120,12 @@ export class PushNotificationService implements OnApplicationShutdown { endpoint: subscription.endpoint, auth: subscription.auth, publickey: subscription.publickey, - }).then(() => { - this.refreshCache(userId); }); } }); } } - @bindThis - public refreshCache(userId: string): void { - this.subscriptionsCache.refresh(userId); - } - @bindThis public dispose(): void { this.subscriptionsCache.dispose(); diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index c4feeaf971..deefb9adfe 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -212,8 +212,8 @@ export class QueryService { // または 自分自身 .orWhere('note.userId = :meId') // または 自分宛て - .orWhere(':meIdAsList <@ note.visibleUserIds') - .orWhere(':meIdAsList <@ note.mentions') + .orWhere(':meId = ANY(note.visibleUserIds)') + .orWhere(':meId = ANY(note.mentions)') .orWhere(new Brackets(qb => { qb // または フォロワー宛ての投稿であり、 @@ -228,7 +228,7 @@ export class QueryService { })); })); - q.setParameters({ meId: me.id, meIdAsList: [me.id] }); + q.setParameters({ meId: me.id }); } } diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 5e11dda78e..37c2ae4100 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -1,36 +1,26 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ +import { setTimeout } from 'node:timers/promises'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { baseQueueOptions, QUEUE } from '@/queue/const.js'; -import { allSettled } from '@/misc/promise-tracker.js'; -import { - DeliverJobData, - EndedPollNotificationJobData, - InboxJobData, - RelationshipJobData, - UserWebhookDeliverJobData, - SystemWebhookDeliverJobData, - ScheduledNoteDeleteJobData -} from '../queue/types.js'; +import { QUEUE, baseQueueOptions } from '@/queue/const.js'; import type { Provider } from '@nestjs/common'; +import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; -export type ScheduledNoteDeleteQueue = Bull.Queue; export type DeliverQueue = Bull.Queue; export type InboxQueue = Bull.Queue; export type DbQueue = Bull.Queue; export type RelationshipQueue = Bull.Queue; export type ObjectStorageQueue = Bull.Queue; -export type UserWebhookDeliverQueue = Bull.Queue; -export type SystemWebhookDeliverQueue = Bull.Queue; +export type WebhookDeliverQueue = Bull.Queue; const $system: Provider = { provide: 'queue:system', @@ -44,12 +34,6 @@ const $endedPollNotification: Provider = { inject: [DI.config, DI.redisForJobQueue], }; -const $scheduledNoteDeleted: Provider = { - provide: 'queue:scheduledNoteDelete', - useFactory: (config: Config, redisForJobQueue: Redis.Redis) => new Bull.Queue(QUEUE.SCHEDULED_NOTE_DELETE, baseQueueOptions(config, QUEUE.SCHEDULED_NOTE_DELETE, redisForJobQueue)), - inject: [DI.config, DI.redisForJobQueue], -} - const $deliver: Provider = { provide: 'queue:deliver', useFactory: (config: Config, redisForJobQueue: Redis.Redis) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER, redisForJobQueue)), @@ -80,15 +64,9 @@ const $objectStorage: Provider = { inject: [DI.config, DI.redisForJobQueue], }; -const $userWebhookDeliver: Provider = { - provide: 'queue:userWebhookDeliver', - useFactory: (config: Config, redisForJobQueue: Redis.Redis) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER, redisForJobQueue)), - inject: [DI.config, DI.redisForJobQueue], -}; - -const $systemWebhookDeliver: Provider = { - provide: 'queue:systemWebhookDeliver', - useFactory: (config: Config, redisForJobQueue: Redis.Redis) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER, redisForJobQueue)), +const $webhookDeliver: Provider = { + provide: 'queue:webhookDeliver', + useFactory: (config: Config, redisForJobQueue: Redis.Redis) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER, redisForJobQueue)), inject: [DI.config, DI.redisForJobQueue], }; @@ -98,57 +76,54 @@ const $systemWebhookDeliver: Provider = { providers: [ $system, $endedPollNotification, - $scheduledNoteDeleted, $deliver, $inbox, $db, $relationship, $objectStorage, - $userWebhookDeliver, - $systemWebhookDeliver, + $webhookDeliver, ], exports: [ $system, $endedPollNotification, - $scheduledNoteDeleted, $deliver, $inbox, $db, $relationship, $objectStorage, - $userWebhookDeliver, - $systemWebhookDeliver, + $webhookDeliver, ], }) export class QueueModule implements OnApplicationShutdown { constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, - @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) {} public async dispose(): Promise { - // Wait for all potential queue jobs - await allSettled(); - // And then close all queues + if (process.env.NODE_ENV === 'test') { + // XXX: + // Shutting down the existing connections causes errors on Jest as + // Misskey has asynchronous postgres/redis connections that are not + // awaited. + // Let's wait for some random time for them to finish. + await setTimeout(5000); + } await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), - this.scheduledNoteDeleteQueue.close(), this.deliverQueue.close(), this.inboxQueue.close(), this.dbQueue.close(), this.relationshipQueue.close(), this.objectStorageQueue.close(), - this.userWebhookDeliverQueue.close(), - this.systemWebhookDeliverQueue.close(), + this.webhookDeliverQueue.close(), ]); } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 8596d6ca06..466cd94f16 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -9,32 +9,12 @@ import type { IActivity } from '@/core/activitypub/type.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; -import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; -import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; -import type { - DbJobData, - DeliverJobData, - RelationshipJobData, - SystemWebhookDeliverJobData, - ThinUser, - UserWebhookDeliverJobData, -} from '../queue/types.js'; -import type { - DbQueue, - DeliverQueue, - EndedPollNotificationQueue, - InboxQueue, - ObjectStorageQueue, - RelationshipQueue, - SystemQueue, - UserWebhookDeliverQueue, - SystemWebhookDeliverQueue, - ScheduledNoteDeleteQueue, -} from './QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; +import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; @@ -46,14 +26,12 @@ export class QueueService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, - @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) { this.systemQueue.add('tickCharts', { }, { @@ -97,15 +75,11 @@ export class QueueService { if (content == null) return null; if (to == null) return null; - const contentBody = JSON.stringify(content); - const digest = ApRequestCreator.createDigest(contentBody); - const data: DeliverJobData = { user: { id: user.id, }, - content: contentBody, - digest, + content, to, isSharedInbox, }; @@ -130,8 +104,6 @@ export class QueueService { @bindThis public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map) { if (content == null) return null; - const contentBody = JSON.stringify(content); - const digest = ApRequestCreator.createDigest(contentBody); const opts = { attempts: this.config.deliverJobMaxAttempts ?? 12, @@ -146,8 +118,7 @@ export class QueueService { name: d[0], data: { user, - content: contentBody, - digest, + content, to: d[0], isSharedInbox: d[1], }, @@ -204,16 +175,6 @@ export class QueueService { }); } - @bindThis - public createExportClipsJob(user: ThinUser) { - return this.dbQueue.add('exportClips', { - user: { id: user.id }, - }, { - removeOnComplete: true, - removeOnFail: true, - }); - } - @bindThis public createExportFavoritesJob(user: ThinUser) { return this.dbQueue.add('exportFavorites', { @@ -458,13 +419,9 @@ export class QueueService { }); } - /** - * @see UserWebhookDeliverJobData - * @see WebhookDeliverProcessorService - */ @bindThis - public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) { - const data: UserWebhookDeliverJobData = { + public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) { + const data = { type, content, webhookId: webhook.id, @@ -475,33 +432,7 @@ export class QueueService { eventId: randomUUID(), }; - return this.userWebhookDeliverQueue.add(webhook.id, data, { - attempts: 4, - backoff: { - type: 'custom', - }, - removeOnComplete: true, - removeOnFail: true, - }); - } - - /** - * @see SystemWebhookDeliverJobData - * @see WebhookDeliverProcessorService - */ - @bindThis - public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) { - const data: SystemWebhookDeliverJobData = { - type, - content, - webhookId: webhook.id, - to: webhook.url, - secret: webhook.secret, - createdAt: Date.now(), - eventId: randomUUID(), - }; - - return this.systemWebhookDeliverQueue.add(webhook.id, data, { + return this.webhookDeliverQueue.add(webhook.id, data, { attempts: 4, backoff: { type: 'custom', diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 371207c33a..f5766a145a 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -28,15 +28,13 @@ import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { RoleService } from '@/core/RoleService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; -const FALLBACK = '\u2764'; +const FALLBACK = '❤'; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; const legacies: Record = { 'like': '👍', - 'love': '\u2764', // ハート、異体字セレクタを入れない + 'love': '❤', // ここに記述する場合は異体字セレクタを入れない 'laugh': '😆', 'hmm': '🤔', 'surprise': '😮', @@ -105,8 +103,6 @@ export class ReactionService { @bindThis public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) { - const meta = await this.metaService.fetch(); - // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -120,16 +116,11 @@ export class ReactionService { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } - // Check if note is Renote - if (isRenote(note) && !isQuote(note)) { - throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.'); - } - let reaction = _reaction ?? FALLBACK; if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { - reaction = '\u2764'; - } else if (_reaction != null) { + reaction = '❤️'; + } else if (_reaction) { const custom = reaction.match(isCustomEmojiRegexp); if (custom) { const reacterHost = this.utilityService.toPunyNullable(user.host); @@ -150,11 +141,6 @@ export class ReactionService { if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) { reaction = FALLBACK; } - - // for media silenced host, custom emoji reactions are not allowed - if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) { - reaction = FALLBACK; - } } else { // リアクションとして使う権限がない reaction = FALLBACK; @@ -227,6 +213,8 @@ export class ReactionService { } } + const meta = await this.metaService.fetch(); + if (meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserReactionsChart.update(user, note); } @@ -280,7 +268,7 @@ export class ReactionService { } } - trackPromise(dm.execute()); + dm.execute(); } //#endregion } @@ -328,41 +316,40 @@ export class ReactionService { dm.addDirectRecipe(reactee as MiRemoteUser); } dm.addFollowersRecipe(); - trackPromise(dm.execute()); + dm.execute(); } //#endregion } - /** - * 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、 - * データベース上には存在する「0個のリアクションがついている」という情報を削除する。 - */ @bindThis - public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] { - return Object.entries(reactions) - .filter(([, count]) => { - // `ReactionService.prototype.delete`ではリアクション削除時に、 - // `MiNote['reactions']`のエントリの値をデクリメントしているが、 - // デクリメントしているだけなのでエントリ自体は0を値として持つ形で残り続ける。 - // そのため、この処理がなければ、「0個のリアクションがついている」ということになってしまう。 - return count > 0; - }) - .map(([reaction, count]) => { - // unchecked indexed access - const convertedReaction = legacies[reaction] as string | undefined; + public convertLegacyReactions(reactions: Record) { + const _reactions = {} as Record; - const key = this.decodeReaction(convertedReaction ?? reaction).reaction; + for (const reaction of Object.keys(reactions)) { + if (reactions[reaction] <= 0) continue; - return [key, count] as const; - }) - .reduce((acc, [key, count]) => { - // unchecked indexed access - const prevCount = acc[key] as number | undefined; + if (Object.keys(legacies).includes(reaction)) { + if (_reactions[legacies[reaction]]) { + _reactions[legacies[reaction]] += reactions[reaction]; + } else { + _reactions[legacies[reaction]] = reactions[reaction]; + } + } else { + if (_reactions[reaction]) { + _reactions[reaction] += reactions[reaction]; + } else { + _reactions[reaction] = reactions[reaction]; + } + } + } - acc[key] = (prevCount ?? 0) + count; + const _reactions2 = {} as Record; + + for (const reaction of Object.keys(_reactions)) { + _reactions2[this.decodeReaction(reaction).reaction] = _reactions[reaction]; + } - return acc; - }, {}); + return _reactions2; } @bindThis diff --git a/packages/backend/src/core/RegistryApiService.ts b/packages/backend/src/core/RegistryApiService.ts index 2c8877d8a8..98fafb4e24 100644 --- a/packages/backend/src/core/RegistryApiService.ts +++ b/packages/backend/src/core/RegistryApiService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 89b067de02..81e4132b59 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -53,11 +53,11 @@ export class RelayService { @bindThis public async addRelay(inbox: string): Promise { - const relay = await this.relaysRepository.insertOne({ + const relay = await this.relaysRepository.insert({ id: this.idService.gen(), inbox, status: 'requesting', - }); + }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0])); const relayActor = await this.getRelayActor(); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); diff --git a/packages/backend/src/core/RemoteLoggerService.ts b/packages/backend/src/core/RemoteLoggerService.ts index 413b03bb56..b121d82a24 100644 --- a/packages/backend/src/core/RemoteLoggerService.ts +++ b/packages/backend/src/core/RemoteLoggerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index f5a55eb8bc..c181b92b8f 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 7860c868dc..3597cb4325 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -36,7 +36,6 @@ export type RolePolicies = { ltlAvailable: boolean; canPublicNote: boolean; canEditNote: boolean; - mentionLimit: number; canInvite: boolean; inviteLimit: number; inviteLimitCycle: number; @@ -44,12 +43,10 @@ export type RolePolicies = { canManageCustomEmojis: boolean; canManageAvatarDecorations: boolean; canSearchNotes: boolean; - canAdvancedSearchNotes: boolean; canUseTranslator: boolean; canHideAds: boolean; driveCapacityMb: number; alwaysMarkNsfw: boolean; - canUpdateBioMedia: boolean; pinLimit: number; antennaLimit: number; wordMuteLimit: number; @@ -60,7 +57,6 @@ export type RolePolicies = { userEachUserListsLimit: number; rateLimitFactor: number; avatarDecorationLimit: number; - fileSizeLimit: number; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -68,7 +64,6 @@ export const DEFAULT_POLICIES: RolePolicies = { ltlAvailable: true, canPublicNote: true, canEditNote: true, - mentionLimit: 20, canInvite: false, inviteLimit: 0, inviteLimitCycle: 60 * 24 * 7, @@ -76,12 +71,10 @@ export const DEFAULT_POLICIES: RolePolicies = { canManageCustomEmojis: false, canManageAvatarDecorations: false, canSearchNotes: false, - canAdvancedSearchNotes: false, canUseTranslator: true, canHideAds: false, driveCapacityMb: 100, alwaysMarkNsfw: false, - canUpdateBioMedia: true, pinLimit: 5, antennaLimit: 5, wordMuteLimit: 200, @@ -92,7 +85,6 @@ export const DEFAULT_POLICIES: RolePolicies = { userEachUserListsLimit: 50, rateLimitFactor: 1, avatarDecorationLimit: 1, - fileSizeLimit: 50, }; @Injectable() @@ -187,11 +179,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { case 'userRoleAssigned': { const cached = this.roleAssignmentByUserIdCache.get(body.userId); if (cached) { - cached.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい + cached.push({ ...body, expiresAt: body.expiresAt ? new Date(body.expiresAt) : null, - user: null, // joinなカラムは通常取ってこないので - role: null, // joinなカラムは通常取ってこないので }); } break; @@ -210,82 +200,45 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean { + private evalCond(user: MiUser, value: RoleCondFormulaValue): boolean { try { switch (value.type) { - // ~かつ~ case 'and': { - return value.values.every(v => this.evalCond(user, roles, v)); + return value.values.every(v => this.evalCond(user, v)); } - // ~または~ case 'or': { - return value.values.some(v => this.evalCond(user, roles, v)); + return value.values.some(v => this.evalCond(user, v)); } - // ~ではない case 'not': { - return !this.evalCond(user, roles, value.value); - } - // マニュアルロールがアサインされている - case 'roleAssignedTo': { - return roles.some(r => r.id === value.roleId); + return !this.evalCond(user, value.value); } - // ローカルユーザのみ case 'isLocal': { return this.userEntityService.isLocalUser(user); } - // リモートユーザのみ case 'isRemote': { return this.userEntityService.isRemoteUser(user); } - // サスペンド済みユーザである - case 'isSuspended': { - return user.isSuspended; - } - // 鍵アカウントユーザである - case 'isLocked': { - return user.isLocked; - } - // botユーザである - case 'isBot': { - return user.isBot; - } - // 猫である - case 'isCat': { - return user.isCat; - } - // 「ユーザを見つけやすくする」が有効なアカウント - case 'isExplorable': { - return user.isExplorable; - } - // ユーザが作成されてから指定期間経過した case 'createdLessThan': { return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000)); } - // ユーザが作成されてから指定期間経っていない case 'createdMoreThan': { return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000)); } - // フォロワー数が指定値以下 case 'followersLessThanOrEq': { return user.followersCount <= value.value; } - // フォロワー数が指定値以上 case 'followersMoreThanOrEq': { return user.followersCount >= value.value; } - // フォロー数が指定値以下 case 'followingLessThanOrEq': { return user.followingCount <= value.value; } - // フォロー数が指定値以上 case 'followingMoreThanOrEq': { return user.followingCount >= value.value; } - // ノート数が指定値以下 case 'notesLessThanOrEq': { return user.notesCount <= value.value; } - // ノート数が指定値以上 case 'notesMoreThanOrEq': { return user.notesCount >= value.value; } @@ -319,7 +272,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { const assigns = await this.getUserAssigns(userId); const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; - const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula)); + const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); return [...assignedRoles, ...matchedCondRoles]; } @@ -332,13 +285,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); // 期限切れのロールを除外 assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); + const assignedRoleIds = assigns.map(x => x.roleId); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); - const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge); + const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); if (badgeCondRoles.length > 0) { const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; - const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula)); + const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; } else { return assignedBadgeRoles; @@ -373,7 +326,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canEditNote: calc('canEditNote', vs => vs.some(v => v === true)), - mentionLimit: calc('mentionLimit', vs => Math.max(...vs)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), @@ -381,12 +333,10 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), - canAdvancedSearchNotes: calc('canAdvancedSearchNotes', vs => vs.some(v => v === true)), canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), - canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)), pinLimit: calc('pinLimit', vs => Math.max(...vs)), antennaLimit: calc('antennaLimit', vs => Math.max(...vs)), wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)), @@ -397,7 +347,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), - fileSizeLimit: calc('fileSizeLimit', vs => Math.max(...vs)), }; } @@ -422,32 +371,14 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise { + public async getModeratorIds(includeAdmins = true): Promise { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - const moderatorRoles = includeAdmins - ? roles.filter(r => r.isModerator || r.isAdministrator) - : roles.filter(r => r.isModerator); - + const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); + const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ + roleId: In(moderatorRoles.map(r => r.id)), + }) : []; // TODO: isRootなアカウントも含める - const assigns = moderatorRoles.length > 0 - ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) }) - : []; - - const now = Date.now(); - const result = [ - // Setを経由して重複を除去(ユーザIDは重複する可能性があるので) - ...new Set( - assigns - .filter(it => - (excludeExpire) - ? (it.expiresAt == null || it.expiresAt.getTime() > now) - : true, - ) - .map(a => a.userId), - ), - ]; - - return result.sort((x, y) => x.localeCompare(y)); + return assigns.map(a => a.userId); } @bindThis @@ -501,12 +432,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } } - const created = await this.roleAssignmentsRepository.insertOne({ + const created = await this.roleAssignmentsRepository.insert({ id: this.idService.gen(now), expiresAt: expiresAt, roleId: roleId, userId: userId, - }); + }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); this.rolesRepository.update(roleId, { lastUsedAt: new Date(), @@ -514,15 +445,14 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { this.globalEventService.publishInternalEvent('userRoleAssigned', created); - const user = await this.usersRepository.findOneByOrFail({ id: userId }); - - if (role.isPublic && user.host === null) { + if (role.isPublic) { this.notificationService.createNotification(userId, 'roleAssigned', { roleId: roleId, }); } if (moderator) { + const user = await this.usersRepository.findOneByOrFail({ id: userId }); this.moderationLogService.log(moderator, 'assignRole', { roleId: roleId, roleName: role.name, @@ -589,7 +519,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @bindThis public async create(values: Partial, moderator?: MiUser): Promise { const date = new Date(); - const created = await this.rolesRepository.insertOne({ + const created = await this.rolesRepository.insert({ id: this.idService.gen(date.getTime()), updatedAt: date, lastUsedAt: date, @@ -607,7 +537,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canEditMembersByModerator: values.canEditMembersByModerator, displayOrder: values.displayOrder, policies: values.policies, - }); + }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('roleCreated', created); diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 8150153f0d..3985772ca6 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 748dae1867..1db602fbe6 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -33,12 +33,46 @@ type Q = { op: 'or', qs: Q[] } | { op: 'not', q: Q }; +function compileValue(value: V): string { + if (typeof value === 'string') { + return `'${value}'`; // TODO: escape + } else if (typeof value === 'number') { + return value.toString(); + } else if (typeof value === 'boolean') { + return value.toString(); + } + throw new Error('unrecognized value'); +} + +function compileQuery(q: Q): string { + switch (q.op) { + case '=': return `(${q.k} = ${compileValue(q.v)})`; + case '!=': return `(${q.k} != ${compileValue(q.v)})`; + case '>': return `(${q.k} > ${compileValue(q.v)})`; + case '<': return `(${q.k} < ${compileValue(q.v)})`; + case '>=': return `(${q.k} >= ${compileValue(q.v)})`; + case '<=': return `(${q.k} <= ${compileValue(q.v)})`; + case 'and': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' AND ') })`; + case 'or': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' OR ') })`; + case 'is null': return `(${q.k} IS NULL)`; + case 'is not null': return `(${q.k} IS NOT NULL)`; + case 'not': return `(NOT ${compileQuery(q.q)})`; + default: throw new Error('unrecognized query operator'); + } +} + @Injectable() export class SearchService { + private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local'; + private meilisearchNoteIndex: Index | null = null; + constructor( @Inject(DI.config) private config: Config, + @Inject(DI.meilisearch) + private meilisearch: MeiliSearch | null, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -46,29 +80,146 @@ export class SearchService { private queryService: QueryService, private idService: IdService, ) { + if (meilisearch) { + this.meilisearchNoteIndex = meilisearch.index(`${config.meilisearch!.index}---notes`); + this.meilisearchNoteIndex.updateSettings({ + searchableAttributes: [ + 'text', + 'cw', + ], + sortableAttributes: [ + 'createdAt', + ], + filterableAttributes: [ + 'createdAt', + 'userId', + 'userHost', + 'channelId', + 'tags', + ], + typoTolerance: { + enabled: false, + }, + pagination: { + maxTotalHits: 10000, + }, + }); + } + + if (config.meilisearch?.scope) { + this.meilisearchIndexScope = config.meilisearch.scope; + } } - /** - * TODO: - * 1. FTSの処理を書く - * 2. PGroongaの統合(Advanced Search廃止によるもの) - */ + @bindThis + public async indexNote(note: MiNote): Promise { + if (note.text == null && note.cw == null) return; + if (!['home', 'public'].includes(note.visibility)) return; + + if (this.meilisearch) { + switch (this.meilisearchIndexScope) { + case 'global': + break; + + case 'local': + if (note.userHost == null) 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, + }], { + primaryKey: 'id', + }); + } + } + + @bindThis + public async unindexNote(note: MiNote): Promise { + if (!['home', 'public'].includes(note.visibility)) return; + + if (this.meilisearch) { + 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; - fileOption?: string | null; - excludeNsfw?: boolean; - excludeBot?: boolean; + origin?: string | null; }, pagination: { untilId?: MiNote['id']; sinceId?: MiNote['id']; limit?: number; }): Promise { + if (this.meilisearch) { + 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.origin === 'local') { + filter.qs.push({ op: 'is null', k: 'userHost' }); + } else if (opts.origin === 'remote') { + filter.qs.push({ op: 'is not null', k: 'userHost' }); + } + if (opts.host) { + if (opts.host === '.') { + filter.qs.push({ op: 'is null', k: 'userHost' }); + } else { + filter.qs.push({ op: '=', k: 'userHost', v: opts.host }); + } + } + const res = await this.meilisearchNoteIndex!.search(q, { + sort: ['createdAt: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(), new Set()]; + 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); + } else { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId); + if (opts.origin === 'local') { + query.andWhere('note.userHost IS NULL'); + } else if (opts.origin === 'remote') { + query.andWhere('note.userHost IS NOT NULL'); + } + if (opts.userId) { query.andWhere('note.userId = :userId', { userId: opts.userId }); } else if (opts.channelId) { @@ -81,9 +232,7 @@ export class SearchService { .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('user.isIndexable = true') - .andWhere('user.isSensitive = false'); + .leftJoinAndSelect('renote.user', 'renoteUser'); if (opts.host) { if (opts.host === '.') { @@ -93,36 +242,11 @@ export class SearchService { } } - if (opts.fileOption) { - if (opts.fileOption === 'fileOnly') { - query.andWhere('note.fileIds != \'{}\' ') - } else if (opts.fileOption === 'noFile') { - query.andWhere('note.fileIds = \'{}\' ') - } - } - - if (opts.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE )'); - } - - if (opts.excludeBot) { - query.andWhere(' (SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE '); - } - - /** - * if (this.config.pgroonga) { - * query.andWhere('note.text &@~ :q', { q: `%${sqlLikeEscape(q)}%` }); - *} else { - * query.andWhere('note.text ILIKE :q', { q: `%${sqlLikeEscape(q)}%` }); - *} - * TODO: PGroongaの統合 - */ - 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(); + } } } diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 1c1c3da62b..3ceeb6b8d6 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { generateKeyPair } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import { hashPassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { DataSource, IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; @@ -16,12 +16,10 @@ import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUsedUsername } from '@/models/UsedUsername.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; import { MetaService } from '@/core/MetaService.js'; -import { UserService } from '@/core/UserService.js'; @Injectable() export class SignupService { @@ -36,11 +34,9 @@ export class SignupService { private usedUsernamesRepository: UsedUsernamesRepository, private utilityService: UtilityService, - private userService: UserService, private userEntityService: UserEntityService, private idService: IdService, private metaService: MetaService, - private instanceActorService: InstanceActorService, private usersChart: UsersChart, ) { } @@ -68,23 +64,24 @@ export class SignupService { } // Generate hash of password - hash = await hashPassword(password); + const salt = await bcrypt.genSalt(8); + hash = await bcrypt.hash(password, salt); } // Generate secret const secret = generateUserToken(); // Check username duplication - if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { + if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { throw new Error('DUPLICATED_USERNAME'); } // Check deleted username duplication - if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) { + if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) { throw new Error('USED_USERNAME'); } - const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); + const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0; if (!opts.ignorePreservedUsernames && !isTheFirstUser) { const instance = await this.metaService.fetch(true); @@ -149,8 +146,7 @@ export class SignupService { })); }); - this.usersChart.update(account, true).then(); - this.userService.notifySystemWebhook(account, 'userCreated').then(); + this.usersChart.update(account, true); return { account, secret }; } diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts deleted file mode 100644 index bc6851f788..0000000000 --- a/packages/backend/src/core/SystemWebhookService.ts +++ /dev/null @@ -1,233 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import type { MiUser, SystemWebhooksRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; -import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js'; -import { IdService } from '@/core/IdService.js'; -import { QueueService } from '@/core/QueueService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import Logger from '@/logger.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; - -@Injectable() -export class SystemWebhookService implements OnApplicationShutdown { - private logger: Logger; - private activeSystemWebhooksFetched = false; - private activeSystemWebhooks: MiSystemWebhook[] = []; - - constructor( - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - @Inject(DI.systemWebhooksRepository) - private systemWebhooksRepository: SystemWebhooksRepository, - 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 - public async fetchActiveSystemWebhooks() { - if (!this.activeSystemWebhooksFetched) { - this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({ - isActive: true, - }); - this.activeSystemWebhooksFetched = true; - } - - return this.activeSystemWebhooks; - } - - /** - * SystemWebhook の一覧を取得する. - */ - @bindThis - public async fetchSystemWebhooks(params?: { - ids?: MiSystemWebhook['id'][]; - isActive?: MiSystemWebhook['isActive']; - on?: MiSystemWebhook['on']; - }): Promise { - const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook'); - if (params) { - if (params.ids && params.ids.length > 0) { - query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids }); - } - if (params.isActive !== undefined) { - query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive }); - } - if (params.on && params.on.length > 0) { - query.andWhere(':on <@ systemWebhook.on', { on: params.on }); - } - } - - return query.getMany(); - } - - /** - * SystemWebhook を作成する. - */ - @bindThis - public async createSystemWebhook( - params: { - isActive: MiSystemWebhook['isActive']; - name: MiSystemWebhook['name']; - on: MiSystemWebhook['on']; - url: MiSystemWebhook['url']; - secret: MiSystemWebhook['secret']; - }, - updater: MiUser, - ): Promise { - const id = this.idService.gen(); - await this.systemWebhooksRepository.insert({ - ...params, - id, - }); - - const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id }); - this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook); - this.moderationLogService - .log(updater, 'createSystemWebhook', { - systemWebhookId: webhook.id, - webhook: webhook, - }) - .then(); - - return webhook; - } - - /** - * SystemWebhook を更新する. - */ - @bindThis - public async updateSystemWebhook( - params: { - id: MiSystemWebhook['id']; - isActive: MiSystemWebhook['isActive']; - name: MiSystemWebhook['name']; - on: MiSystemWebhook['on']; - url: MiSystemWebhook['url']; - secret: MiSystemWebhook['secret']; - }, - updater: MiUser, - ): Promise { - const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id }); - await this.systemWebhooksRepository.update(beforeEntity.id, { - updatedAt: new Date(), - isActive: params.isActive, - name: params.name, - on: params.on, - url: params.url, - secret: params.secret, - }); - - const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id }); - this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity); - this.moderationLogService - .log(updater, 'updateSystemWebhook', { - systemWebhookId: beforeEntity.id, - before: beforeEntity, - after: afterEntity, - }) - .then(); - - return afterEntity; - } - - /** - * SystemWebhook を削除する. - */ - @bindThis - public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) { - const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id }); - await this.systemWebhooksRepository.delete(id); - - this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook); - this.moderationLogService - .log(updater, 'deleteSystemWebhook', { - systemWebhookId: webhook.id, - webhook, - }) - .then(); - } - - /** - * SystemWebhook をWebhook配送キューに追加する - * @see QueueService.systemWebhookDeliver - */ - @bindThis - public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) { - const webhookEntity = typeof webhook === 'string' - ? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook) - : webhook; - if (!webhookEntity || !webhookEntity.isActive) { - this.logger.info(`Webhook is not active or not found : ${webhook}`); - return; - } - - if (!webhookEntity.on.includes(type)) { - this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`); - return; - } - - return this.queueService.systemWebhookDeliver(webhookEntity, type, content); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - if (obj.channel !== 'internal') { - return; - } - - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'systemWebhookCreated': { - if (body.isActive) { - this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body)); - } - break; - } - case 'systemWebhookUpdated': { - if (body.isActive) { - const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id); - if (i > -1) { - this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body); - } else { - this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body)); - } - } else { - this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id); - } - break; - } - case 'systemWebhookDeleted': { - this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id); - break; - } - default: - break; - } - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/UserAuthService.ts b/packages/backend/src/core/UserAuthService.ts index bdc27cbe8e..54575e3c1d 100644 --- a/packages/backend/src/core/UserAuthService.ts +++ b/packages/backend/src/core/UserAuthService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 2f1310b8ef..0320ef7f29 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -16,7 +16,7 @@ import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { WebhookService } from '@/core/WebhookService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; @@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, - private webhookService: UserWebhookService, + private webhookService: WebhookService, private apRendererService: ApRendererService, private loggerService: LoggerService, ) { @@ -109,19 +109,19 @@ export class UserBlockingService implements OnModuleInit { if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(followee, followee, { - schema: 'MeDetailed', + detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } if (this.userEntityService.isLocalUser(follower) && !silent) { this.userEntityService.pack(followee, follower, { - schema: 'UserDetailedNotMe', + detail: true, }).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', { + this.queueService.webhookDeliver(webhook, 'unfollow', { user: packed, }); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 6aab8fde70..36c28af1d0 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js import type { Packed } from '@/misc/json-schema.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { WebhookService } from '@/core/WebhookService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; @@ -30,7 +30,6 @@ import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; -import type { ThinUser } from '@/queue/types.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -82,7 +81,7 @@ export class UserFollowingService implements OnModuleInit { private metaService: MetaService, private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, - private webhookService: UserWebhookService, + private webhookService: WebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, private fanoutTimelineService: FanoutTimelineService, @@ -95,35 +94,21 @@ export class UserFollowingService implements OnModuleInit { this.userBlockingService = this.moduleRef.get('UserBlockingService'); } - @bindThis - public async deliverAccept(follower: MiRemoteUser, followee: MiPartialLocalUser, requestId?: string) { - const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); - this.queueService.deliver(followee, content, follower.inbox, false); - } - @bindThis public async follow( - _follower: ThinUser, - _followee: ThinUser, + _follower: { id: MiUser['id'] }, + _followee: { id: MiUser['id'] }, { requestId, silent = false, withReplies }: { requestId?: string, silent?: boolean, withReplies?: boolean, } = {}, ): Promise { - /** - * 必ず最新のユーザー情報を取得する - */ const [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: _follower.id }), this.usersRepository.findOneByOrFail({ id: _followee.id }), ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isRemoteUser(followee)) { - // What? - throw new Error('Remote user cannot follow remote user.'); - } - // check blocking const [blocking, blocked] = await Promise.all([ this.userBlockingService.checkBlocked(follower.id, followee.id), @@ -144,24 +129,6 @@ export class UserFollowingService implements OnModuleInit { if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); } - if (await this.followingsRepository.exists({ - where: { - followerId: follower.id, - followeeId: followee.id, - }, - })) { - // すでにフォロー関係が存在している場合 - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - // リモート → ローカル: acceptを送り返しておしまい - this.deliverAccept(follower, followee, requestId); - return; - } - if (this.userEntityService.isLocalUser(follower)) { - // ローカル → リモート/ローカル: 例外 - throw new IdentifiableError('ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced', 'already following'); - } - } - const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); // フォロー対象が鍵アカウントである or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or @@ -177,7 +144,7 @@ export class UserFollowingService implements OnModuleInit { let autoAccept = false; // 鍵アカウントであっても、既にフォローされていた場合はスルー - const isFollowing = await this.followingsRepository.exists({ + const isFollowing = await this.followingsRepository.exist({ where: { followerId: follower.id, followeeId: followee.id, @@ -189,7 +156,7 @@ export class UserFollowingService implements OnModuleInit { // フォローしているユーザーは自動承認オプション if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { - const isFollowed = await this.followingsRepository.exists({ + const isFollowed = await this.followingsRepository.exist({ where: { followerId: followee.id, followeeId: follower.id, @@ -203,7 +170,7 @@ export class UserFollowingService implements OnModuleInit { if (followee.isLocked && !autoAccept) { autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs( follower, - (oldSrc, newSrc) => this.followingsRepository.exists({ + (oldSrc, newSrc) => this.followingsRepository.exist({ where: { followeeId: followee.id, followerId: newSrc.id, @@ -222,7 +189,8 @@ export class UserFollowingService implements OnModuleInit { await this.insertFollowingDoc(followee, follower, silent, withReplies); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.deliverAccept(follower, followee, requestId); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + this.queueService.deliver(followee, content, follower.inbox, false); } } @@ -265,7 +233,7 @@ export class UserFollowingService implements OnModuleInit { this.cacheService.userFollowingsCache.refresh(follower.id); - const requestExist = await this.followRequestsRepository.exists({ + const requestExist = await this.followRequestsRepository.exist({ where: { followeeId: followee.id, followerId: follower.id, @@ -279,10 +247,8 @@ export class UserFollowingService implements OnModuleInit { }); // 通知を作成 - if (follower.host === null) { - this.notificationService.createNotification(follower.id, 'followRequestAccepted', { - }, followee.id); - } + this.notificationService.createNotification(follower.id, 'followRequestAccepted', { + }, followee.id); } if (alreadyFollowed) return; @@ -327,13 +293,13 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isLocalUser(follower) && !silent) { // Publish follow event this.userEntityService.pack(followee.id, follower, { - schema: 'UserDetailedNotMe', + detail: true, }).then(async packed => { - this.globalEventService.publishMainStream(follower.id, 'follow', packed); + this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); 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', { + this.queueService.webhookDeliver(webhook, 'follow', { user: packed, }); } @@ -347,7 +313,7 @@ export class UserFollowingService implements OnModuleInit { 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', { + this.queueService.webhookDeliver(webhook, 'followed', { user: packed, }); } @@ -394,13 +360,13 @@ export class UserFollowingService implements OnModuleInit { if (!silent && this.userEntityService.isLocalUser(follower)) { // Publish unfollow event this.userEntityService.pack(followee.id, follower, { - schema: 'UserDetailedNotMe', + detail: true, }).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', { + this.queueService.webhookDeliver(webhook, 'unfollow', { user: packed, }); } @@ -513,13 +479,7 @@ export class UserFollowingService implements OnModuleInit { if (blocking) throw new Error('blocking'); if (blocked) throw new Error('blocked'); - // Remove old follow requests before creating a new one. - await this.followRequestsRepository.delete({ - followeeId: followee.id, - followerId: follower.id, - }); - - const followRequest = await this.followRequestsRepository.insertOne({ + const followRequest = await this.followRequestsRepository.insert({ id: this.idService.gen(), followerId: follower.id, followeeId: followee.id, @@ -533,14 +493,14 @@ export class UserFollowingService implements OnModuleInit { followeeHost: followee.host, followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined, followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined, - }); + }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0])); // Publish receiveRequest event if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed)); this.userEntityService.pack(followee.id, followee, { - schema: 'MeDetailed', + detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); // 通知を作成 @@ -571,7 +531,7 @@ export class UserFollowingService implements OnModuleInit { } } - const requestExist = await this.followRequestsRepository.exists({ + const requestExist = await this.followRequestsRepository.exist({ where: { followeeId: followee.id, followerId: follower.id, @@ -588,7 +548,7 @@ export class UserFollowingService implements OnModuleInit { }); this.userEntityService.pack(followee.id, followee, { - schema: 'MeDetailed', + detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } @@ -611,11 +571,12 @@ export class UserFollowingService implements OnModuleInit { await this.insertFollowingDoc(followee, follower, false, request.withReplies); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.deliverAccept(follower, followee as MiPartialLocalUser, request.requestId ?? undefined); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee)); + this.queueService.deliver(followee, content, follower.inbox, false); } this.userEntityService.pack(followee.id, followee, { - schema: 'MeDetailed', + detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); } @@ -735,14 +696,14 @@ export class UserFollowingService implements OnModuleInit { @bindThis private async publishUnfollow(followee: Both, follower: Local): Promise { const packedFollowee = await this.userEntityService.pack(followee.id, follower, { - schema: 'UserDetailedNotMe', + detail: true, }); 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', { + this.queueService.webhookDeliver(webhook, 'unfollow', { user: packedFollowee, }); } diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 51ac99179a..b372d834e5 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 6333356fe9..0ce6182769 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit { const currentCount = await this.userListMembershipsRepository.countBy({ userListId: list.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { throw new UserListService.TooManyUsersError(); } diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts index 06643be5fb..df40a210b9 100644 --- a/packages/backend/src/core/UserMutingService.ts +++ b/packages/backend/src/core/UserMutingService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/UserRenoteMutingService.ts b/packages/backend/src/core/UserRenoteMutingService.ts deleted file mode 100644 index bdc5e23f4b..0000000000 --- a/packages/backend/src/core/UserRenoteMutingService.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import type { RenoteMutingsRepository } from '@/models/_.js'; -import type { MiRenoteMuting } from '@/models/RenoteMuting.js'; - -import { IdService } from '@/core/IdService.js'; -import type { MiUser } from '@/models/User.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { CacheService } from '@/core/CacheService.js'; - -@Injectable() -export class UserRenoteMutingService { - constructor( - @Inject(DI.renoteMutingsRepository) - private renoteMutingsRepository: RenoteMutingsRepository, - - private idService: IdService, - private cacheService: CacheService, - ) { - } - - @bindThis - public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise { - await this.renoteMutingsRepository.insert({ - id: this.idService.gen(), - muterId: user.id, - muteeId: target.id, - }); - - await this.cacheService.renoteMutingsCache.refresh(user.id); - } - - @bindThis - public async unmute(mutings: MiRenoteMuting[]): Promise { - if (mutings.length === 0) return; - - await this.renoteMutingsRepository.delete({ - id: In(mutings.map(m => m.id)), - }); - - const muterIds = [...new Set(mutings.map(m => m.muterId))]; - for (const muterId of muterIds) { - await this.cacheService.renoteMutingsCache.refresh(muterId); - } - } -} diff --git a/packages/backend/src/core/UserSearchService.ts b/packages/backend/src/core/UserSearchService.ts deleted file mode 100644 index 0d03cf6ee0..0000000000 --- a/packages/backend/src/core/UserSearchService.ts +++ /dev/null @@ -1,205 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Brackets, SelectQueryBuilder } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; -import type { Config } from '@/config.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { Packed } from '@/misc/json-schema.js'; - -function defaultActiveThreshold() { - return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30); -} - -@Injectable() -export class UserSearchService { - constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private userEntityService: UserEntityService, - ) { - } - - /** - * ユーザ名とホスト名によるユーザ検索を行う. - * - * - 検索結果には優先順位がつけられており、以下の順序で検索が行われる. - * 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ - * 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ - * 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ - * 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ - * - ログインしていない場合は、以下の順序で検索が行われる. - * 1. 一定期間以内に更新されたユーザ - * 2. 一定期間以内に更新されていないユーザ - * - それぞれの検索結果はユーザ名の昇順でソートされる. - * - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが). - * (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される) - * - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される. - * - ユーザ名の検索は大文字小文字を区別しない. - * - ホスト名の検索は大文字小文字を区別しない. - * - 検索結果は最大で {@link opts.limit} 件までとなる. - * - * ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す. - * - * @param params 検索条件. - * @param opts 関数の動作を制御するオプション. - * @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない. - * @see {@link UserSearchService#buildSearchUserQueries} - * @see {@link UserSearchService#buildSearchUserNoLoginQueries} - */ - @bindThis - public async search( - params: { - username?: string | null, - host?: string | null, - activeThreshold?: Date, - }, - opts?: { - limit?: number, - detail?: boolean, - }, - me?: MiUser | null, - ): Promise[]> { - const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params); - - let resultSet = new Set(); - const limit = opts?.limit ?? 10; - for (const query of queries) { - const ids = await query - .select('user.id') - .limit(limit - resultSet.size) - .orderBy('user.usernameLower', 'ASC') - .getRawMany<{ user_id: MiUser['id'] }>() - .then(res => res.map(x => x.user_id)); - - resultSet = new Set([...resultSet, ...ids]); - if (resultSet.size >= limit) { - break; - } - } - - return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>( - [...resultSet].slice(0, limit), - me, - { schema: opts?.detail ? 'UserDetailed' : 'UserLite' }, - ); - } - - /** - * ログイン済みユーザによる検索実行時のクエリ一覧を構築する. - * @param me - * @param params - * @private - */ - @bindThis - private buildSearchUserQueries( - me: MiUser, - params: { - username?: string | null, - host?: string | null, - activeThreshold?: Date, - }, - ) { - // デフォルト30日以内に更新されたユーザーをアクティブユーザーとする - const activeThreshold = params.activeThreshold ?? defaultActiveThreshold(); - - const followingUserQuery = this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - const activeFollowingUsersQuery = this.generateUserQueryBuilder(params) - .andWhere(`user.id IN (${followingUserQuery.getQuery()})`) - .andWhere('user.updatedAt > :activeThreshold', { activeThreshold }); - activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters()); - - const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params) - .andWhere(`user.id IN (${followingUserQuery.getQuery()})`) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); - })); - inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters()); - - // 自分自身がヒットするとしたらここ - const activeUserQuery = this.generateUserQueryBuilder(params) - .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`) - .andWhere('user.updatedAt > :activeThreshold', { activeThreshold }); - activeUserQuery.setParameters(followingUserQuery.getParameters()); - - const inactiveUserQuery = this.generateUserQueryBuilder(params) - .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`) - .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); - inactiveUserQuery.setParameters(followingUserQuery.getParameters()); - - return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery]; - } - - /** - * ログインしていないユーザによる検索実行時のクエリ一覧を構築する. - * @param params - * @private - */ - @bindThis - private buildSearchUserNoLoginQueries(params: { - username?: string | null, - host?: string | null, - activeThreshold?: Date, - }) { - // デフォルト30日以内に更新されたユーザーをアクティブユーザーとする - const activeThreshold = params.activeThreshold ?? defaultActiveThreshold(); - - const activeUserQuery = this.generateUserQueryBuilder(params) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold }); - })); - - const inactiveUserQuery = this.generateUserQueryBuilder(params) - .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); - - return [activeUserQuery, inactiveUserQuery]; - } - - /** - * ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する. - * @param params - * @private - */ - @bindThis - private generateUserQueryBuilder(params: { - username?: string | null, - host?: string | null, - }): SelectQueryBuilder { - const userQuery = this.usersRepository.createQueryBuilder('user'); - - if (params.username) { - userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' }); - } - - if (params.host) { - if (params.host === this.config.hostname || params.host === '.') { - userQuery.andWhere('user.host IS NULL'); - } else { - userQuery.andWhere('user.host LIKE :host', { - host: sqlLikeEscape(params.host.toLowerCase()) + '%', - }); - } - } - - userQuery.andWhere('user.isSuspended = FALSE'); - - return userQuery; - } -} diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts index 9b1961c631..79fb4c77ca 100644 --- a/packages/backend/src/core/UserService.ts +++ b/packages/backend/src/core/UserService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,18 +8,15 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; @Injectable() export class UserService { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - private systemWebhookService: SystemWebhookService, - private userEntityService: UserEntityService, ) { } @@ -53,23 +50,4 @@ export class UserService { }); } } - - /** - * SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する. - * ここではJobQueueへのエンキューのみを行うため、即時実行されない. - * - * @see SystemWebhookService.enqueueSystemWebhook - */ - @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, - ); - } - } } diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index d594a223f4..342e0f7987 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts deleted file mode 100644 index e96bfeea95..0000000000 --- a/packages/backend/src/core/UserWebhookService.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import type { WebhooksRepository } from '@/models/_.js'; -import type { MiWebhook } 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'; - -@Injectable() -export class UserWebhookService implements OnApplicationShutdown { - private activeWebhooksFetched = false; - private activeWebhooks: MiWebhook[] = []; - - constructor( - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - @Inject(DI.webhooksRepository) - private webhooksRepository: WebhooksRepository, - ) { - this.redisForSub.on('message', this.onMessage); - } - - @bindThis - public async getActiveWebhooks() { - if (!this.activeWebhooksFetched) { - this.activeWebhooks = await this.webhooksRepository.findBy({ - active: true, - }); - this.activeWebhooksFetched = true; - } - - return this.activeWebhooks; - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - if (obj.channel !== 'internal') { - return; - } - - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'webhookCreated': { - if (body.active) { - this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinなカラムは通常取ってこないので - }); - } - break; - } - case 'webhookUpdated': { - if (body.active) { - const i = this.activeWebhooks.findIndex(a => a.id === body.id); - if (i > -1) { - this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinなカラムは通常取ってこないので - }; - } else { - this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinなカラムは通常取ってこないので - }); - } - } else { - this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id); - } - break; - } - case 'webhookDeleted': { - this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id); - break; - } - default: - break; - } - } - - @bindThis - public dispose(): void { - this.redisForSub.off('message', this.onMessage); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 94729250a6..6f2b398bbb 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -43,33 +43,13 @@ export class UtilityService { } @bindThis - public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean { - if (!silencedHosts || host == null) return false; - return silencedHosts.some(x => host.toLowerCase() === x); - } - - @bindThis - public concatNoteContentsForKeyWordCheck(content: { - cw?: string | null; - text?: string | null; - pollChoices?: string[] | null; - others?: string[] | null; - }): string { - /** - * ノートの内容を結合してキーワードチェック用の文字列を生成する - * cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする - */ - return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`; - } - - @bindThis - public isKeyWordIncluded(text: string, keyWords: string[]): boolean { - if (keyWords.length === 0) return false; + public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean { + if (sensitiveWords.length === 0) return false; if (text === '') return false; const regexpregexp = /^\/(.+)\/(.*)$/; - const matched = keyWords.some(filter => { + const matched = sensitiveWords.some(filter => { // represents RegExp const regexp = filter.match(regexpregexp); // This should never happen due to input sanitisation. diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 747fe4fc7e..4490563ec9 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index ec9f4484a4..1ad740f83a 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,7 @@ import { generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server'; -import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers'; +import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers'; import { DI } from '@/di-symbols.js'; import type { UserSecurityKeysRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -26,7 +26,7 @@ import type { PublicKeyCredentialDescriptorFuture, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, -} from '@simplewebauthn/types'; +} from '@simplewebauthn/typescript-types'; @Injectable() export class WebAuthnService { @@ -49,7 +49,7 @@ export class WebAuthnService { const instance = await this.metaService.fetch(); return { origin: this.config.url, - rpId: this.config.hostname, + rpId: this.config.host, rpName: instance.name ?? this.config.host, rpIcon: instance.iconUrl ?? undefined, }; @@ -65,12 +65,13 @@ export class WebAuthnService { const registrationOptions = await generateRegistrationOptions({ rpName: relyingParty.rpName, rpID: relyingParty.rpId, - userID: isoUint8Array.fromUTF8String(userId), + userID: userId, userName: userName, userDisplayName: userDisplayName, attestationType: 'indirect', - excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{ - id: key.id, + excludeCredentials: keys.map(key => ({ + id: Buffer.from(key.id, 'base64url'), + type: 'public-key', transports: key.transports ?? undefined, })), authenticatorSelection: { @@ -86,7 +87,7 @@ export class WebAuthnService { @bindThis public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{ - credentialID: string; + credentialID: Uint8Array; credentialPublicKey: Uint8Array; attestationObject: Uint8Array; fmt: AttestationFormat; @@ -143,7 +144,6 @@ export class WebAuthnService { @bindThis public async initiateAuthentication(userId: MiUser['id']): Promise { - const relyingParty = await this.getRelyingParty(); const keys = await this.userSecurityKeysRepository.findBy({ userId: userId, }); @@ -153,9 +153,9 @@ export class WebAuthnService { } const authenticationOptions = await generateAuthenticationOptions({ - rpID: relyingParty.rpId, - allowCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{ - id: key.id, + allowCredentials: keys.map(key => ({ + id: Buffer.from(key.id, 'base64url'), + type: 'public-key', transports: key.transports ?? undefined, })), userVerification: 'preferred', @@ -191,7 +191,7 @@ export class WebAuthnService { if (cert[0] === 0x04) { // 前の実装ではいつも 0x04 で始まっていた const halfLength = (cert.length - 1) / 2; - const cborMap = new Map(); + const cborMap = new Map(); cborMap.set(1, 2); // kty, EC2 cborMap.set(3, -7); // alg, ES256 cborMap.set(-1, 1); // crv, P256 @@ -219,7 +219,7 @@ export class WebAuthnService { expectedOrigin: relyingParty.origin, expectedRPID: relyingParty.rpId, authenticator: { - credentialID: key.id, + credentialID: Buffer.from(key.id, 'base64url'), credentialPublicKey: Buffer.from(key.publicKey, 'base64url'), counter: key.counter, transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index c18f64f1f3..abdb791174 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts new file mode 100644 index 0000000000..7d0de2a5a9 --- /dev/null +++ b/packages/backend/src/core/WebhookService.ts @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import type { WebhooksRepository } from '@/models/_.js'; +import type { MiWebhook } from '@/models/Webhook.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +@Injectable() +export class WebhookService implements OnApplicationShutdown { + private webhooksFetched = false; + private webhooks: MiWebhook[] = []; + + constructor( + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, + + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + ) { + //this.onMessage = this.onMessage.bind(this); + this.redisForSub.on('message', this.onMessage); + } + + @bindThis + public async getActiveWebhooks() { + if (!this.webhooksFetched) { + this.webhooks = await this.webhooksRepository.findBy({ + active: true, + }); + this.webhooksFetched = true; + } + + return this.webhooks; + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'webhookCreated': + if (body.active) { + this.webhooks.push({ + ...body, + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + }); + } + break; + case 'webhookUpdated': + if (body.active) { + const i = this.webhooks.findIndex(a => a.id === body.id); + if (i > -1) { + this.webhooks[i] = { + ...body, + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + }; + } else { + this.webhooks.push({ + ...body, + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + }); + } + } else { + this.webhooks = this.webhooks.filter(a => a.id !== body.id); + } + break; + case 'webhookDeleted': + this.webhooks = this.webhooks.filter(a => a.id !== body.id); + break; + default: + break; + } + } + } + + @bindThis + public dispose(): void { + this.redisForSub.off('message', this.onMessage); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 2a37f427a4..206b8c7a0a 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,7 +13,7 @@ import { ApPersonService } from './models/ApPersonService.js'; import type { ApObject } from './type.js'; import type { Resolver } from './ApResolverService.js'; -type Visibility = 'public' | 'home' | 'followers' | 'specified' | 'private'; +type Visibility = 'public' | 'home' | 'followers' | 'specified'; type AudienceInfo = { visibility: Visibility, @@ -40,7 +40,7 @@ export class ApAudienceService { const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), - )).filter(x => x != null); + )).filter((x): x is MiUser => x != null); if (toGroups.public.length > 0) { return { @@ -58,7 +58,7 @@ export class ApAudienceService { }; } - if (toGroups.followers.length > 0 || ccGroups.followers.length > 0) { + if (toGroups.followers.length > 0) { return { visibility: 'followers', mentionedUsers, diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 6ae8c55f05..b793929b00 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -127,12 +127,12 @@ export class ApDbResolverService implements OnApplicationShutdown { return await this.cacheService.userByIdCache.fetchMaybe( parsed.id, - () => this.usersRepository.findOneBy({ id: parsed.id, isDeleted: false }).then(x => x ?? undefined), + () => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined), ) as MiLocalUser | undefined ?? null; } else { return await this.cacheService.uriPersonCache.fetch( parsed.uri, - () => this.usersRepository.findOneBy({ uri: parsed.uri, isDeleted: false }), + () => this.usersRepository.findOneBy({ uri: parsed.uri }), ) as MiRemoteUser | null; } } @@ -157,12 +157,8 @@ export class ApDbResolverService implements OnApplicationShutdown { if (key == null) return null; - const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null; - if (user == null) return null; - if (user.isDeleted) return null; - return { - user, + user: await this.cacheService.findUserById(key.userId) as MiRemoteUser, key, }; } @@ -176,7 +172,6 @@ export class ApDbResolverService implements OnApplicationShutdown { key: MiUserPublickey | null; } | null> { const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; - if (user.isDeleted) return null; const key = await this.publicKeyByUserIdCache.fetch( user.id, diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 5d07cd8e8f..939de242bb 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -144,7 +144,7 @@ class DeliverManager { } // deliver - await this.queueService.deliverMany(this.actor, this.activity, inboxes); + this.queueService.deliverMany(this.actor, this.activity, inboxes); } } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 8b4fd29cab..568031f532 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -26,11 +26,10 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; import { MessagingService } from '@/core/MessagingService.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import type { MiRemoteUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; @@ -40,7 +39,7 @@ import { ApAudienceService } from './ApAudienceService.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; -import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IRead, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; +import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IRead, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js'; @Injectable() export class ApInboxService { @@ -62,6 +61,9 @@ export class ApInboxService { @Inject(DI.messagingMessagesRepository) private messagingMessagesRepository: MessagingMessagesRepository, + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -70,7 +72,6 @@ export class ApInboxService { private utilityService: UtilityService, private idService: IdService, private metaService: MetaService, - private abuseReportService: AbuseReportService, private userFollowingService: UserFollowingService, private apAudienceService: ApAudienceService, private reactionService: ReactionService, @@ -88,37 +89,28 @@ export class ApInboxService { private apPersonService: ApPersonService, private apQuestionService: ApQuestionService, private queueService: QueueService, - private globalEventService: GlobalEventService, private messagingService: MessagingService, + private globalEventService: GlobalEventService, ) { this.logger = this.apLoggerService.logger; } @bindThis - public async performActivity(actor: MiRemoteUser, activity: IObject): Promise { - let result = undefined as string | void; + public async performActivity(actor: MiRemoteUser, activity: IObject): Promise { if (isCollectionOrOrderedCollection(activity)) { - const results = [] as [string, string | void][]; const resolver = this.apResolverService.createResolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { const act = await resolver.resolve(item); try { - results.push([getApId(item), await this.performOneActivity(actor, act)]); + await this.performOneActivity(actor, act); } catch (err) { if (err instanceof Error || typeof err === 'string') { this.logger.error(err); - } else { - throw err; } } } - - const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok'))); - if (hasReason) { - result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n'); - } } else { - result = await this.performOneActivity(actor, activity); + await this.performOneActivity(actor, activity); } // ついでにリモートユーザーの情報が古かったら更新しておく @@ -129,45 +121,44 @@ export class ApInboxService { }); } } - return result; } @bindThis - public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise { + public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise { if (actor.isSuspended) return; if (isCreate(activity)) { - return await this.create(actor, activity); + await this.create(actor, activity); } else if (isDelete(activity)) { - return await this.delete(actor, activity); + await this.delete(actor, activity); } else if (isUpdate(activity)) { - return await this.update(actor, activity); + await this.update(actor, activity); } else if (isRead(activity)) { - return await this.read(actor, activity); + await this.read(actor, activity); } else if (isFollow(activity)) { - return await this.follow(actor, activity); + await this.follow(actor, activity); } else if (isAccept(activity)) { - return await this.accept(actor, activity); + await this.accept(actor, activity); } else if (isReject(activity)) { - return await this.reject(actor, activity); + await this.reject(actor, activity); } else if (isAdd(activity)) { - return await this.add(actor, activity); + await this.add(actor, activity).catch(err => this.logger.error(err)); } else if (isRemove(activity)) { - return await this.remove(actor, activity); + await this.remove(actor, activity).catch(err => this.logger.error(err)); } else if (isAnnounce(activity)) { - return await this.announce(actor, activity); + await this.announce(actor, activity); } else if (isLike(activity)) { - return await this.like(actor, activity); + await this.like(actor, activity); } else if (isUndo(activity)) { - return await this.undo(actor, activity); + await this.undo(actor, activity); } else if (isBlock(activity)) { - return await this.block(actor, activity); + await this.block(actor, activity); } else if (isFlag(activity)) { - return await this.flag(actor, activity); + await this.flag(actor, activity); } else if (isMove(activity)) { - return await this.move(actor, activity); + await this.move(actor, activity); } else { - return `unrecognized activity type: ${activity.type}`; + this.logger.warn(`unrecognized activity type: ${activity.type}`); } } @@ -272,49 +263,38 @@ export class ApInboxService { } @bindThis - private async add(actor: MiRemoteUser, activity: IAdd): Promise { + private async add(actor: MiRemoteUser, activity: IAdd): Promise { if (actor.uri !== activity.actor) { - return 'invalid actor'; + throw new Error('invalid actor'); } if (activity.target == null) { - return 'target is null'; + throw new Error('target is null'); } if (activity.target === actor.featured) { const note = await this.apNoteService.resolveNote(activity.object); - if (note == null) return 'note not found'; + if (note == null) throw new Error('note not found'); await this.notePiningService.addPinned(actor, note.id); return; } - return `unknown target: ${activity.target}`; + throw new Error(`unknown target: ${activity.target}`); } @bindThis - private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise { + private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); this.logger.info(`Announce: ${uri}`); - const resolver = this.apResolverService.createResolver(); - - if (!activity.object) return 'skip: activity has no object property'; const targetUri = getApId(activity.object); - if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; - - const target = await resolver.resolve(activity.object).catch(e => { - this.logger.error(`Resolution failed: ${e}`); - return e; - }); - if (isPost(target)) return await this.announceNote(actor, activity, target); - - return `skip: unknown object type ${getApType(target)}`; + this.announceNote(actor, activity, targetUri); } @bindThis - private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise { + private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); if (actor.isSuspended) { @@ -332,7 +312,7 @@ export class ApInboxService { try { // 既に同じURIを持つものが登録されていないかチェック - const exist = await this.apNoteService.fetchNote(fromRelay ? target : uri); + const exist = await this.apNoteService.fetchNote(fromRelay ? targetUri : uri); if (exist) { return; } @@ -340,21 +320,24 @@ export class ApInboxService { // Announce対象をresolve let renote; try { - renote = await this.apNoteService.resolveNote(target); - if (renote == null) return 'announce target is null'; + renote = await this.apNoteService.resolveNote(targetUri); + if (renote == null) throw new Error('announce target is null'); } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { - if (!err.isRetryable) { - return `Ignored announce target ${target.id} - ${err.statusCode}`; + if (err.isClientError) { + this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`); + return; } - return `Error in announce target ${target.id} - ${err.statusCode}`; + + this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`); } throw err; } if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) { - return 'skip: invalid actor for this activity'; + this.logger.warn('skip: invalid actor for this activity'); + return; } if (fromRelay) { @@ -369,7 +352,8 @@ export class ApInboxService { const createdAt = activity.published ? new Date(activity.published) : null; if (createdAt && createdAt < this.idService.parse(renote.id).date) { - return 'skip: malformed createdAt'; + this.logger.warn('skip: malformed createdAt'); + return; } await this.noteCreateService.create(actor, { @@ -403,15 +387,11 @@ export class ApInboxService { } @bindThis - private async create(actor: MiRemoteUser, activity: ICreate): Promise { + private async create(actor: MiRemoteUser, activity: ICreate): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); - if (!activity.object) return 'skip: activity has no object property'; - const targetUri = getApId(activity.object); - if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; - // copy audiences between activity <=> object. if (typeof activity.object === 'object') { const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); @@ -436,9 +416,9 @@ export class ApInboxService { }); if (isPost(object)) { - await this.createNote(resolver, actor, object, false, activity); + this.createNote(resolver, actor, object, false, activity); } else { - return `Unknown type: ${getApType(object)}`; + this.logger.warn(`Unknown type: ${getApType(object)}`); } } @@ -467,7 +447,7 @@ export class ApInboxService { await this.apNoteService.createNote(note, resolver, silent); return 'ok'; } catch (err) { - if (err instanceof StatusError && !err.isRetryable) { + if (err instanceof StatusError && err.isClientError) { return `skip ${err.statusCode}`; } else { throw err; @@ -480,7 +460,7 @@ export class ApInboxService { @bindThis private async delete(actor: MiRemoteUser, activity: IDelete): Promise { if (actor.uri !== activity.actor) { - return 'invalid actor'; + throw new Error('invalid actor'); } // 削除対象objectのtype @@ -540,8 +520,6 @@ export class ApInboxService { isDeleted: true, }); - this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id }); - return `ok: queued ${job.name} ${job.id}`; } @@ -587,19 +565,22 @@ export class ApInboxService { const userIds = uris .filter(uri => uri.startsWith(this.config.url + '/users/')) .map(uri => uri.split('/').at(-1)) - .filter(x => x != null); + .filter((userId): userId is string => userId !== undefined); const users = await this.usersRepository.findBy({ id: In(userIds), }); if (users.length < 1) return 'skip'; - await this.abuseReportService.report([{ + const report = await this.abuseUserReportsRepository.insert({ + id: this.idService.gen(), targetUserId: users[0].id, targetUserHost: users[0].host, reporterId: actor.id, reporterHost: actor.host, comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`, - }]); + }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); + + this.queueService.createReportAbuseJob(report); return 'ok'; } @@ -647,29 +628,29 @@ export class ApInboxService { } @bindThis - private async remove(actor: MiRemoteUser, activity: IRemove): Promise { + private async remove(actor: MiRemoteUser, activity: IRemove): Promise { if (actor.uri !== activity.actor) { - return 'invalid actor'; + throw new Error('invalid actor'); } if (activity.target == null) { - return 'target is null'; + throw new Error('target is null'); } if (activity.target === actor.featured) { const note = await this.apNoteService.resolveNote(activity.object); - if (note == null) return 'note not found'; + if (note == null) throw new Error('note not found'); await this.notePiningService.removePinned(actor, note.id); return; } - return `unknown target: ${activity.target}`; + throw new Error(`unknown target: ${activity.target}`); } @bindThis private async undo(actor: MiRemoteUser, activity: IUndo): Promise { if (actor.uri !== activity.actor) { - return 'invalid actor'; + throw new Error('invalid actor'); } const uri = activity.id ?? activity; @@ -680,7 +661,7 @@ export class ApInboxService { const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); - return e; + throw e; }); // don't queue because the sender may attempt again when timeout @@ -700,7 +681,7 @@ export class ApInboxService { return 'skip: follower not found'; } - const isFollowing = await this.followingsRepository.exists({ + const isFollowing = await this.followingsRepository.exist({ where: { followerId: follower.id, followeeId: actor.id, @@ -757,14 +738,14 @@ export class ApInboxService { return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません'; } - const requestExist = await this.followRequestsRepository.exists({ + const requestExist = await this.followRequestsRepository.exist({ where: { followerId: actor.id, followeeId: followee.id, }, }); - const isFollowing = await this.followingsRepository.exists({ + const isFollowing = await this.followingsRepository.exist({ where: { followerId: actor.id, followeeId: followee.id, diff --git a/packages/backend/src/core/activitypub/ApLoggerService.ts b/packages/backend/src/core/activitypub/ApLoggerService.ts index 428d8061ce..06677b32cc 100644 --- a/packages/backend/src/core/activitypub/ApLoggerService.ts +++ b/packages/backend/src/core/activitypub/ApLoggerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index 51400a0951..ceb1297def 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -25,21 +25,8 @@ export class ApMfmService { } @bindThis - public getNoteHtml(note: Pick, apAppend?: string) { - let noMisskeyContent = false; - const srcMfm = (note.text ?? '') + (apAppend ?? ''); - - const parsed = mfm.parse(srcMfm); - - if (!apAppend && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) { - noMisskeyContent = true; - } - - const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers)); - - return { - content, - noMisskeyContent, - }; + public getNoteHtml(note: MiNote): string | null { + if (!note.text) return ''; + return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); } } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 032de43af6..fd745a2a22 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -27,10 +27,10 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, EventsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; -import { JsonLdService } from './JsonLdService.js'; +import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; -import { CONTEXT } from './misc/contexts.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IRead, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @Injectable() @@ -60,7 +60,7 @@ export class ApRendererService { private customEmojiService: CustomEmojiService, private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, - private jsonLdService: JsonLdService, + private ldSignatureService: LdSignatureService, private userKeypairService: UserKeypairService, private apMfmService: ApMfmService, private mfmService: MfmService, @@ -171,7 +171,6 @@ export class ApRendererService { mediaType: file.webpublicType ?? file.type, url: this.driveFileEntityService.getPublicUrl(file, undefined, true), name: file.comment, - sensitive: file.isSensitive, }; } @@ -321,7 +320,7 @@ export class ApRendererService { const getPromisedFiles = async (ids: string[]): Promise => { if (ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter(x => x != null); + return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null); }; let inReplyTo; @@ -331,7 +330,7 @@ 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 inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } }); if (inReplyToUserExist) { if (inReplyToNote.uri) { @@ -395,15 +394,17 @@ export class ApRendererService { poll = await this.pollsRepository.findOneBy({ noteId: note.id }); } - let apAppend = ''; + let apText = text; if (quote) { - apAppend += `\n\nRE: ${quote}`; + apText += `\n\nRE: ${quote}`; } const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend); + const content = this.apMfmService.getNoteHtml(Object.assign({}, note, { + text: apText, + })); const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); @@ -416,6 +417,9 @@ export class ApRendererService { const asPoll = poll ? { type: 'Question', + content: this.apMfmService.getNoteHtml(Object.assign({}, note, { + text: text, + })), [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ type: 'Note', @@ -449,13 +453,11 @@ export class ApRendererService { attributedTo, summary: summary ?? undefined, content: content ?? undefined, - ...(noMisskeyContent ? {} : { - _misskey_content: text, - source: { - content: text, - mediaType: 'text/x.misskeymarkdown', - }, - }), + _misskey_content: text, + source: { + content: text, + mediaType: 'text/x.misskeymarkdown', + }, _misskey_quote: quote, quoteUrl: quote, published: this.idService.parse(note.id).date.toISOString(), @@ -525,7 +527,6 @@ export class ApRendererService { discoverable: user.isExplorable, publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, - isIndexable: user.isIndexable, attachment: attachment.length ? attachment : undefined, }; @@ -652,16 +653,48 @@ export class ApRendererService { x.id = `${this.config.url}/${randomUUID()}`; } - return Object.assign({ '@context': CONTEXT }, x as T & { id: string }); + return Object.assign({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { + // as non-standards + manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', + sensitive: 'as:sensitive', + Hashtag: 'as:Hashtag', + quoteUrl: 'as:quoteUrl', + // Mastodon + toot: 'http://joinmastodon.org/ns#', + Emoji: 'toot:Emoji', + featured: 'toot:featured', + discoverable: 'toot:discoverable', + // schema + schema: 'http://schema.org#', + PropertyValue: 'schema:PropertyValue', + value: 'schema:value', + // Misskey + misskey: 'https://misskey-hub.net/ns#', + '_misskey_content': 'misskey:_misskey_content', + '_misskey_quote': 'misskey:_misskey_quote', + '_misskey_reaction': 'misskey:_misskey_reaction', + '_misskey_votes': 'misskey:_misskey_votes', + '_misskey_summary': 'misskey:_misskey_summary', + '_misskey_talk': 'misskey:_misskey_talk', + 'isCat': 'misskey:isCat', + // vcard + vcard: 'http://www.w3.org/2006/vcard/ns#', + }, + ], + }, x as T & { id: string }); } @bindThis public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise { const keypair = await this.userKeypairService.getUserKeypair(user.id); - const jsonLd = this.jsonLdService.use(); - jsonLd.debug = false; - activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); + const ldSignature = this.ldSignatureService.use(); + ldSignature.debug = false; + activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); return activity; } @@ -719,7 +752,7 @@ export class ApRendererService { if (names.length === 0) return []; const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); - const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null); + const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); return emojis; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 93ac8ce9a7..5ce1e5347b 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,7 +14,6 @@ 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'; type Request = { url: string; @@ -35,9 +34,9 @@ type PrivateKey = { }; export class ApRequestCreator { - static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record }): Signed { + static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }): Signed { const u = new URL(args.url); - const digestHeader = args.digest ?? this.createDigest(args.body); + const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; const request: Request = { url: u.href, @@ -60,10 +59,6 @@ export class ApRequestCreator { }; } - static createDigest(body: string) { - return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`; - } - static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }): Signed { const u = new URL(args.url); @@ -71,7 +66,7 @@ export class ApRequestCreator { url: u.href, method: 'GET', headers: this.#objectAssignWithLcKey({ - 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Accept': 'application/activity+json, application/ld+json', 'Date': new Date().toUTCString(), 'Host': new URL(args.url).host, }, args.additionalHeaders), @@ -150,8 +145,8 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise { - const body = typeof object === 'string' ? object : JSON.stringify(object); + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise { + const body = JSON.stringify(object); const keypair = await this.userKeypairService.getUserKeypair(user.id); @@ -162,7 +157,6 @@ export class ApRequestService { }, url, body, - digest, additionalHeaders: { }, }); @@ -196,9 +190,6 @@ export class ApRequestService { const res = await this.httpRequestService.send(url, { method: req.request.method, headers: req.request.headers, - }, { - throwErrorWhenResponseNotOk: true, - validators: [validateContentTypeSetAsActivityPub], }); return await res.json(); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index bb3c40f093..d63669fa42 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -105,7 +105,7 @@ export class Resolver { const object = (this.user ? await this.apRequestService.signedGet(value, this.user) as IObject - : await this.httpRequestService.getActivityJson(value)) as IObject; + : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; if ( Array.isArray(object['@context']) ? diff --git a/packages/backend/src/core/activitypub/JsonLdService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts similarity index 79% rename from packages/backend/src/core/activitypub/JsonLdService.ts rename to packages/backend/src/core/activitypub/LdSignatureService.ts index 100d4fa19f..28083d0891 100644 --- a/packages/backend/src/core/activitypub/JsonLdService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,14 +7,13 @@ import * as crypto from 'node:crypto'; import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; -import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js'; -import { validateContentTypeSetAsJsonLD } from './misc/validator.js'; +import { CONTEXTS } from './misc/contexts.js'; import type { JsonLdDocument } from 'jsonld'; -import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js'; +import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js'; -// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017 +// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 -class JsonLd { +class LdSignature { public debug = false; public preLoad = true; public loderTimeout = 5000; @@ -89,18 +88,10 @@ class JsonLd { } @bindThis - public async compact(data: any, context: any = CONTEXT): Promise { + public async normalize(data: JsonLdDocument): Promise { const customLoader = this.getLoader(); // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 - return (await import('jsonld')).default.compact(data, context, { - documentLoader: customLoader, - }); - } - - @bindThis - public async normalize(data: JsonLdDocument): Promise { - const customLoader = this.getLoader(); return (await import('jsonld')).default.normalize(data, { documentLoader: customLoader, }); @@ -112,11 +103,11 @@ class JsonLd { if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`); if (this.preLoad) { - if (url in PRELOADED_CONTEXTS) { + if (url in CONTEXTS) { if (this.debug) console.debug(`HIT: ${url}`); return { contextUrl: undefined, - document: PRELOADED_CONTEXTS[url], + document: CONTEXTS[url], documentUrl: url, }; } @@ -133,7 +124,7 @@ class JsonLd { } @bindThis - private async fetchDocument(url: string): Promise { + private async fetchDocument(url: string): Promise { const json = await this.httpRequestService.send( url, { @@ -142,10 +133,7 @@ class JsonLd { }, timeout: this.loderTimeout, }, - { - throwErrorWhenResponseNotOk: false, - validators: [validateContentTypeSetAsJsonLD], - }, + { throwErrorWhenResponseNotOk: false }, ).then(res => { if (!res.ok) { throw new Error(`${res.status} ${res.statusText}`); @@ -154,7 +142,7 @@ class JsonLd { } }); - return json as JsonLdObject; + return json as JsonLd; } @bindThis @@ -166,14 +154,14 @@ class JsonLd { } @Injectable() -export class JsonLdService { +export class LdSignatureService { constructor( private httpRequestService: HttpRequestService, ) { } @bindThis - public use(): JsonLd { - return new JsonLd(this.httpRequestService); + public use(): LdSignature { + return new LdSignature(this.httpRequestService); } } diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index d597197dbe..35cc24012e 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Context, JsonLd } from 'jsonld/jsonld-spec.js'; +import type { JsonLd } from 'jsonld/jsonld-spec.js'; /* eslint:disable:quotemark indent */ const id_v1 = { @@ -526,44 +526,7 @@ const activitystreams = { }, } satisfies JsonLd; -const context_iris = [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', -]; - -const extension_context_definition = { - Key: 'sec:Key', - // as non-standards - manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', - sensitive: 'as:sensitive', - Hashtag: 'as:Hashtag', - quoteUrl: 'as:quoteUrl', - // Mastodon - toot: 'http://joinmastodon.org/ns#', - Emoji: 'toot:Emoji', - featured: 'toot:featured', - discoverable: 'toot:discoverable', - indexable: 'toot:indexable', - // schema - schema: 'http://schema.org#', - PropertyValue: 'schema:PropertyValue', - value: 'schema:value', - // Misskey - misskey: 'https://misskey-hub.net/ns#', - '_misskey_content': 'misskey:_misskey_content', - '_misskey_quote': 'misskey:_misskey_quote', - '_misskey_reaction': 'misskey:_misskey_reaction', - '_misskey_votes': 'misskey:_misskey_votes', - '_misskey_summary': 'misskey:_misskey_summary', - '_misskey_talk': 'misskey:_misskey_talk', - 'isCat': 'misskey:isCat', - // vcard - vcard: 'http://www.w3.org/2006/vcard/ns#', -} satisfies Context; - -export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition]; - -export const PRELOADED_CONTEXTS: Record = { +export const CONTEXTS: Record = { 'https://w3id.org/identity/v1': id_v1, 'https://w3id.org/security/v1': security_v1, 'https://www.w3.org/ns/activitystreams': activitystreams, diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts deleted file mode 100644 index 690beeffef..0000000000 --- a/packages/backend/src/core/activitypub/misc/validator.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Response } from 'node-fetch'; - -export function validateContentTypeSetAsActivityPub(response: Response): void { - const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); - - if (contentType === '') { - throw new Error('Validate content type of AP response: No content-type header'); - } - if ( - contentType.startsWith('application/activity+json') || - (contentType.startsWith('application/ld+json;') && contentType.includes('https://www.w3.org/ns/activitystreams')) - ) { - return; - } - throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json'); -} - -const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/; - -export function validateContentTypeSetAsJsonLD(response: Response): void { - const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); - - if (contentType === '') { - throw new Error('Validate content type of JSON LD: No content-type header'); - } - if ( - contentType.startsWith('application/ld+json') || - contentType.startsWith('application/json') || - plusJsonSuffixRegex.test(contentType) - ) { - return; - } - throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json'); -} diff --git a/packages/backend/src/core/activitypub/models/ApEventService.ts b/packages/backend/src/core/activitypub/models/ApEventService.ts index 77cdb0beb9..62b1bd288a 100644 --- a/packages/backend/src/core/activitypub/models/ApEventService.ts +++ b/packages/backend/src/core/activitypub/models/ApEventService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 3691967270..8ddf5df66c 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; -import { isDocument, type IObject } from '../type.js'; +import type { IObject } from '../type.js'; @Injectable() export class ApImageService { @@ -39,7 +39,7 @@ export class ApImageService { * Imageを作成します。 */ @bindThis - public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { + public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); @@ -47,18 +47,16 @@ export class ApImageService { const image = await this.apResolverService.createResolver().resolve(value); - if (!isDocument(image)) return null; - if (image.url == null) { - return null; + throw new Error('invalid image: url not provided'); } if (typeof image.url !== 'string') { - return null; + throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2)); } if (!checkHttps(image.url)) { - return null; + throw new Error('invalid image: unexpected schema of url: ' + image.url); } this.logger.info(`Creating the Image: ${image.url}`); @@ -88,11 +86,12 @@ export class ApImageService { /** * Imageを解決します。 * - * ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise { - // TODO: Misskeyに対象のImageが登録されていればそれを返す + public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise { + // TODO // リモートサーバーからフェッチしてきて登録 return await this.createImage(actor, value); diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts index 2cd151fa04..4b9fc5ed94 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -27,7 +27,7 @@ export class ApMentionService { const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), - )).filter(x => x != null); + )).filter((x): x is MiUser => x != null); return mentionedUsers; } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 2f78d67334..a67f7afd2a 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -1,9 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import promiseLimit from 'promise-limit'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { EmojisRepository, MessagingMessagesRepository, NotesRepository, PollsRepository } from '@/models/_.js'; @@ -25,8 +26,7 @@ import { MessagingService } from '@/core/MessagingService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { NoteUpdateService } from '@/core/NoteUpdateService.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; +import { getApId, getApType, getOneApHrefNullable, getOneApId, isEmoji, validPost } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; import { ApDbResolverService } from '../ApDbResolverService.js'; @@ -92,20 +92,20 @@ export class ApNoteService { const expectHost = this.utilityService.extractDbHost(uri); if (!validPost.includes(getApType(object))) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`); + return new Error(`invalid Note: invalid object type ${getApType(object)}`); } if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); + return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); } const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)); if (object.attributedTo && actualHost !== expectHost) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); + return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed'); + return new Error('invalid Note: published timestamp is malformed'); } return null; @@ -139,7 +139,7 @@ export class ApNoteService { value, object, }); - throw err; + throw new Error('invalid note'); } const note = object as IPost; @@ -163,47 +163,11 @@ export class ApNoteService { throw new Error('invalid note.attributedTo: ' + note.attributedTo); } - const uri = getOneApId(note.attributedTo); - - // ローカルで投稿者を検索し、もし凍結されていたらスキップ - const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser; - if (cachedActor && cachedActor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); - } - - const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); - const apHashtags = extractApHashtags(note.tag); - - const cw = note.summary === '' ? null : note.summary; - - // テキストのパース - let text: string | null = null; - if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { - text = note.source.content; - } else if (typeof note._misskey_content !== 'undefined') { - text = note._misskey_content; - } else if (typeof note.content === 'string') { - text = this.apMfmService.htmlToMfm(note.content, note.tag); - } - - const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); - - //#region Contents Check - // 添付ファイルとユーザーをこのサーバーで登録する前に内容をチェックする - /** - * 禁止ワードチェック - */ - const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); - if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); - } - //#endregion - - const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser; - // 解決した投稿者が凍結されていたらスキップ + // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + throw new Error('actor has been suspended'); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -220,14 +184,19 @@ export class ApNoteService { let isMessaging = note._misskey_talk && visibility === 'specified'; - // 添付ファイル - const files: MiDriveFile[] = []; + const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); + const apHashtags = extractApHashtags(note.tag); - for (const attach of toArray(note.attachment)) { - attach.sensitive ??= note.sensitive; - const file = await this.apImageService.resolveImage(actor, attach); - if (file) files.push(file); - } + // 添付ファイル + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + const limit = promiseLimit(2); + const files = (await Promise.all(toArray(note.attachment).map(attach => ( + limit(() => this.apImageService.resolveImage(actor, { + ...attach, + sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする + })) + )))); // リプライ const reply: MiNote | null = note.inReplyTo @@ -272,12 +241,12 @@ export class ApNoteService { return { status: 'ok', res }; } catch (e) { return { - status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', + status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', }; } }; - const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null)); + const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); 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); @@ -288,6 +257,18 @@ export class ApNoteService { } } + const cw = note.summary === '' ? null : note.summary; + + // テキストのパース + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { + text = note.source.content; + } else if (typeof note._misskey_content !== 'undefined') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = this.apMfmService.htmlToMfm(note.content, note.tag); + } + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); @@ -317,6 +298,7 @@ export class ApNoteService { const apEmojis = emojis.map(emoji => emoji.name); + const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); const event = await this.apEventService.extractEventFromNote(note, resolver).catch(() => undefined); if (isMessaging) { @@ -392,13 +374,13 @@ export class ApNoteService { throw new Error('actor has been suspended'); } - const files: MiDriveFile[] = []; - - for (const attach of toArray(note.attachment)) { - attach.sensitive ??= note.sensitive; - const file = await this.apImageService.resolveImage(actor, attach); - if (file) files.push(file); - } + const limit = promiseLimit(2); + const files = (await Promise.all(toArray(note.attachment).map(attach => ( + limit(() => this.apImageService.resolveImage(actor, { + ...attach, + sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする + })) + )))); const cw = note.summary === '' ? null : note.summary; @@ -522,7 +504,7 @@ export class ApNoteService { this.logger.info(`register emoji host=${host}, name=${name}`); - return await this.emojisRepository.insertOne({ + return await this.emojisRepository.insert({ id: this.idService.gen(), host, name, @@ -531,7 +513,7 @@ export class ApNoteService { publicUrl: tag.icon.url, updatedAt: new Date(), aliases: [], - }); + }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); })); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f472e46c1f..73fddbe04e 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -34,7 +34,6 @@ import { StatusError } from '@/misc/status-error.js'; import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; @@ -103,8 +102,6 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - private roleService: RoleService, - private avatarDecorationService: AvatarDecorationService, ) { } @@ -246,42 +243,20 @@ export class ApPersonService implements OnModuleInit { return null; } - private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise>> { - if (user == null) throw new Error('failed to create user: user is null'); - + private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise> { const [avatar, banner] = await Promise.all([icon, image].map(img => { - // if we have an explicitly missing image, return an - // explicitly-null set of values - if ((img == null) || (typeof img === 'object' && img.url == null)) { - return { id: null, url: null, blurhash: null }; - } - + if (img == null) return null; + if (user == null) throw new Error('failed to create user: user is null'); return this.apImageService.resolveImage(user, img).catch(() => null); })); - if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null)) - && !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) { - return {}; - } - - /* - we don't want to return nulls on errors! if the database fields - are already null, nothing changes; if the database has old - values, we should keep those. The exception is if the remote has - actually removed the images: in that case, the block above - returns the special {id:null}&c value, and we return those - */ return { - ...( avatar ? { - avatarId: avatar.id, - avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null, - avatarBlurhash: avatar.blurhash, - } : {}), - ...( banner ? { - bannerId: banner.id, - bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner) : null, - bannerBlurhash: banner.blurhash, - } : {}), + avatarId: avatar?.id ?? null, + bannerId: banner?.id ?? null, + avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar', false) : null, + bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, undefined, false) : null, + avatarBlurhash: avatar?.blurhash ?? null, + bannerBlurhash: banner?.blurhash ?? null, }; } @@ -428,7 +403,6 @@ export class ApPersonService implements OnModuleInit { tags, isBot, isCat: (person as any).isCat === true, - isIndexable: person.isIndexable ?? true, emojis, })) as MiRemoteUser; @@ -642,7 +616,6 @@ export class ApPersonService implements OnModuleInit { tags, isBot: getApType(object) === 'Service' || getApType(object) === 'Application', isCat: (person as any).isCat === true, - isIndexable: person.isIndexable ?? true, isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, @@ -806,7 +779,7 @@ export class ApPersonService implements OnModuleInit { // とりあえずidを別の時間で生成して順番を維持 let td = 0; - for (const note of featuredNotes.filter(x => x != null)) { + for (const note of featuredNotes.filter((note): note is MiNote => note != null)) { td -= 1000; transactionalEntityManager.insert(MiUserNotePining, { id: this.idService.gen(Date.now() + td), diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 73004d10b0..19b9cffe05 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -51,7 +51,7 @@ export class ApQuestionService { const choices = question[multiple ? 'anyOf' : 'oneOf'] ?.map((x) => x.name) - .filter(x => x != null) + .filter((x): x is string => typeof x === 'string') ?? []; const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); @@ -74,10 +74,10 @@ export class ApQuestionService { //#region このサーバーに既に登録されているか const note = await this.notesRepository.findOneBy({ uri }); - if (note == null) throw new Error('Question is not registered'); + if (note == null) throw new Error('Question is not registed'); const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error('Question is not registered'); + if (poll == null) throw new Error('Question is not registed'); //#endregion // resolve new Question object diff --git a/packages/backend/src/core/activitypub/models/icon.ts b/packages/backend/src/core/activitypub/models/icon.ts index 5722507a3b..84ffa67583 100644 --- a/packages/backend/src/core/activitypub/models/icon.ts +++ b/packages/backend/src/core/activitypub/models/icon.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/activitypub/models/identifier.ts b/packages/backend/src/core/activitypub/models/identifier.ts index dce4f410b4..d52c3ddb71 100644 --- a/packages/backend/src/core/activitypub/models/identifier.ts +++ b/packages/backend/src/core/activitypub/models/identifier.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts index f75cc45f7e..c035915243 100644 --- a/packages/backend/src/core/activitypub/models/tag.ts +++ b/packages/backend/src/core/activitypub/models/tag.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined): return hashtags.map(tag => { const m = tag.name.match(/^#(.+)/); return m ? m[1] : null; - }).filter(x => x != null); + }).filter((x): x is string => x != null); } export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 82949b58c6..fe8c2aa36a 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -26,7 +26,6 @@ export interface IObject { endTime?: Date; icon?: any; image?: any; - mediaType?: string; url?: ApObject | string; href?: string; tag?: IObject | IObject[]; @@ -187,7 +186,6 @@ export interface IActor extends IObject { }; 'vcard:bday'?: string; 'vcard:Address'?: string; - isIndexable?: boolean; } export const isCollection = (object: IObject): object is ICollection => @@ -247,14 +245,14 @@ export interface IKey extends IObject { } export interface IApDocument extends IObject { - type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video'; + type: 'Document'; + name: string | null; + mediaType: string; } -export const isDocument = (object: IObject): object is IApDocument => - ['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object)); - -export interface IApImage extends IApDocument { +export interface IApImage extends IObject { type: 'Image'; + name: string | null; } export interface ICreate extends IActivity { @@ -334,4 +332,3 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move'; -export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note'; diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts index 20815ea968..8c65f6b4ed 100644 --- a/packages/backend/src/core/chart/ChartLoggerService.ts +++ b/packages/backend/src/core/chart/ChartLoggerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,6 +14,6 @@ export class ChartLoggerService { constructor( private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('chart', 'white'); + this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test'); } } diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 79681370a1..b94d089448 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts index 05905f3782..a0fe2230d0 100644 --- a/packages/backend/src/core/chart/charts/active-users.ts +++ b/packages/backend/src/core/chart/charts/active-users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/ap-request.ts b/packages/backend/src/core/chart/charts/ap-request.ts index 04e771a95b..deaa068c0e 100644 --- a/packages/backend/src/core/chart/charts/ap-request.ts +++ b/packages/backend/src/core/chart/charts/ap-request.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts index 613e074a9f..066a2dfd73 100644 --- a/packages/backend/src/core/chart/charts/drive.ts +++ b/packages/backend/src/core/chart/charts/drive.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/active-users.ts b/packages/backend/src/core/chart/charts/entities/active-users.ts index fc2b88a2bb..fb162b96ba 100644 --- a/packages/backend/src/core/chart/charts/entities/active-users.ts +++ b/packages/backend/src/core/chart/charts/entities/active-users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/ap-request.ts b/packages/backend/src/core/chart/charts/entities/ap-request.ts index 93e47e081b..f7e4b230af 100644 --- a/packages/backend/src/core/chart/charts/entities/ap-request.ts +++ b/packages/backend/src/core/chart/charts/entities/ap-request.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/drive.ts b/packages/backend/src/core/chart/charts/entities/drive.ts index 4ea16da38c..4eca570eea 100644 --- a/packages/backend/src/core/chart/charts/entities/drive.ts +++ b/packages/backend/src/core/chart/charts/entities/drive.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/federation.ts b/packages/backend/src/core/chart/charts/entities/federation.ts index 5ed7804343..393bd8145d 100644 --- a/packages/backend/src/core/chart/charts/entities/federation.ts +++ b/packages/backend/src/core/chart/charts/entities/federation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/instance.ts b/packages/backend/src/core/chart/charts/entities/instance.ts index d0cac3e73f..854f80f539 100644 --- a/packages/backend/src/core/chart/charts/entities/instance.ts +++ b/packages/backend/src/core/chart/charts/entities/instance.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/notes.ts b/packages/backend/src/core/chart/charts/entities/notes.ts index 325236ab35..8471544da9 100644 --- a/packages/backend/src/core/chart/charts/entities/notes.ts +++ b/packages/backend/src/core/chart/charts/entities/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/per-user-drive.ts b/packages/backend/src/core/chart/charts/entities/per-user-drive.ts index 25d4619dde..fb7b0490b1 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-drive.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/per-user-following.ts b/packages/backend/src/core/chart/charts/entities/per-user-following.ts index 1618bd22f3..ba2c53923b 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/per-user-notes.ts b/packages/backend/src/core/chart/charts/entities/per-user-notes.ts index 30404b2e48..28ea864720 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/per-user-pv.ts b/packages/backend/src/core/chart/charts/entities/per-user-pv.ts index 7a903afad4..1fd2a62209 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-pv.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts b/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts index bb62bb2386..cd262e7cc4 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/test-grouped.ts b/packages/backend/src/core/chart/charts/entities/test-grouped.ts index 599c1dc136..04d3ce1b2d 100644 --- a/packages/backend/src/core/chart/charts/entities/test-grouped.ts +++ b/packages/backend/src/core/chart/charts/entities/test-grouped.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/test-intersection.ts b/packages/backend/src/core/chart/charts/entities/test-intersection.ts index d29b39716c..f4e57f4af2 100644 --- a/packages/backend/src/core/chart/charts/entities/test-intersection.ts +++ b/packages/backend/src/core/chart/charts/entities/test-intersection.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/test-unique.ts b/packages/backend/src/core/chart/charts/entities/test-unique.ts index bdaa1716ed..1005f26a77 100644 --- a/packages/backend/src/core/chart/charts/entities/test-unique.ts +++ b/packages/backend/src/core/chart/charts/entities/test-unique.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/test.ts b/packages/backend/src/core/chart/charts/entities/test.ts index c80ff55c99..beede6c69f 100644 --- a/packages/backend/src/core/chart/charts/entities/test.ts +++ b/packages/backend/src/core/chart/charts/entities/test.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/entities/users.ts b/packages/backend/src/core/chart/charts/entities/users.ts index f94a5029d7..b75218c159 100644 --- a/packages/backend/src/core/chart/charts/entities/users.ts +++ b/packages/backend/src/core/chart/charts/entities/users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index c2329a2f73..d834198091 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -47,7 +47,7 @@ export default class FederationChart extends Chart { // eslint-di const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance') .select('instance.host') - .where('instance.suspensionState != \'none\''); + .where('instance.isSuspended = true'); const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f') .select('f.followerHost') @@ -89,7 +89,7 @@ export default class FederationChart extends Chart { // eslint-di .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) - .andWhere('instance.suspensionState = \'none\'') + .andWhere('instance.isSuspended = false') .andWhere('instance.isNotResponding = false') .getRawOne() .then(x => parseInt(x.count, 10)), @@ -97,7 +97,7 @@ export default class FederationChart extends Chart { // eslint-di .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) - .andWhere('instance.suspensionState = \'none\'') + .andWhere('instance.isSuspended = false') .andWhere('instance.isNotResponding = false') .getRawOne() .then(x => parseInt(x.count, 10)), diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts index 97f3bc6f2b..a5c92ef2b3 100644 --- a/packages/backend/src/core/chart/charts/instance.ts +++ b/packages/backend/src/core/chart/charts/instance.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts index f763b5fffa..199ca73769 100644 --- a/packages/backend/src/core/chart/charts/notes.ts +++ b/packages/backend/src/core/chart/charts/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts index 404964d8b7..9624cd9aee 100644 --- a/packages/backend/src/core/chart/charts/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/per-user-drive.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts index 588ac638de..0542cf847a 100644 --- a/packages/backend/src/core/chart/charts/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/per-user-following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts index e4900772bb..bffaab8158 100644 --- a/packages/backend/src/core/chart/charts/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts index 31708fefa8..ce85b35c0a 100644 --- a/packages/backend/src/core/chart/charts/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/per-user-pv.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts index c29c4d2870..baca5f1121 100644 --- a/packages/backend/src/core/chart/charts/per-user-reactions.ts +++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts index 7a2844f4ed..7af1f885e9 100644 --- a/packages/backend/src/core/chart/charts/test-grouped.ts +++ b/packages/backend/src/core/chart/charts/test-grouped.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts index b8d0556c9f..94313a5762 100644 --- a/packages/backend/src/core/chart/charts/test-intersection.ts +++ b/packages/backend/src/core/chart/charts/test-intersection.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/test-unique.ts b/packages/backend/src/core/chart/charts/test-unique.ts index f94e008059..2301bb1fdb 100644 --- a/packages/backend/src/core/chart/charts/test-unique.ts +++ b/packages/backend/src/core/chart/charts/test-unique.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/test.ts b/packages/backend/src/core/chart/charts/test.ts index a90dc8f99b..9429d0f2d5 100644 --- a/packages/backend/src/core/chart/charts/test.ts +++ b/packages/backend/src/core/chart/charts/test.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts index d148fc629b..619d254c5f 100644 --- a/packages/backend/src/core/chart/charts/users.ts +++ b/packages/backend/src/core/chart/charts/users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index af5485a46e..1afe28a39a 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,8 +14,7 @@ import { EntitySchema, LessThan, Between } from 'typeorm'; import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { MiRepository, miRepository } from '@/models/_.js'; -import type { DataSource, Repository } from 'typeorm'; +import type { Repository, DataSource } from 'typeorm'; const COLUMN_PREFIX = '___' as const; const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const; @@ -95,29 +94,6 @@ type ToJsonSchema = { }; export function getJsonSchema(schema: S): ToJsonSchema>> { - const unflatten = (str: string, parent: Record) => { - const keys = str.split('.'); - const key = keys.shift(); - const nextKey = keys[0]; - - if (key == null) return; - - if (parent.properties[key] == null) { - parent.properties[key] = nextKey ? { - type: 'object', - properties: {}, - required: [], - } : { - type: 'array', - items: { - type: 'number', - }, - }; - } - - if (nextKey) unflatten(keys.join('.'), parent.properties[key] as Record); - }; - const jsonSchema = { type: 'object', properties: {} as Record, @@ -125,7 +101,10 @@ export function getJsonSchema(schema: S): ToJsonSchema>>; @@ -146,10 +125,10 @@ export default abstract class Chart { group: string | null; }[] = []; // ↓にしたいけどfindOneとかで型エラーになる - //private repositoryForHour: Repository> & MiRepository>; - //private repositoryForDay: Repository> & MiRepository>; - private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>; - private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>; + //private repositoryForHour: Repository>; + //private repositoryForDay: Repository>; + private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>; + private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>; /** * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用) @@ -212,10 +191,6 @@ export default abstract class Chart { } { const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({ name: - span === 'hour' ? `ChartX${name}` : - span === 'day' ? `ChartDayX${name}` : - new Error('not happen') as never, - tableName: span === 'hour' ? `__chart__${camelToSnake(name)}` : span === 'day' ? `__chart_day__${camelToSnake(name)}` : new Error('not happen') as never, @@ -276,8 +251,8 @@ export default abstract class Chart { this.logger = logger; const { hour, day } = Chart.schemaToEntity(name, schema, grouped); - this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>); - this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>); + this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour); + this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day); } @bindThis @@ -392,11 +367,11 @@ export default abstract class Chart { } // 新規ログ挿入 - log = await repository.insertOne({ + log = await repository.insert({ date: date, ...(group ? { group: group } : {}), ...columns, - }) as RawRecord; + }).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord; this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); @@ -464,15 +439,13 @@ export default abstract class Chart { } } - // bake cardinality + // bake unique count for (const [k, v] of Object.entries(finalDiffs)) { if (this.schema[k].uniqueIncrement) { const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns; const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique; - const cardinalityOfHour = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size; - const cardinalityOfDay = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size; - queryForHour[name] = cardinalityOfHour; - queryForDay[name] = cardinalityOfDay; + queryForHour[name] = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size; + queryForDay[name] = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size; } } @@ -644,7 +617,7 @@ export default abstract class Chart { // 要求された範囲にログがひとつもなかったら if (logs.length === 0) { // もっとも新しいログを持ってくる - // (すくなくともひとつログが無いと補間できないため) + // (すくなくともひとつログが無いと隙間埋めできないため) const recentLog = await repository.findOne({ where: group ? { group: group, @@ -661,7 +634,7 @@ export default abstract class Chart { // 要求された範囲の最も古い箇所に位置するログが存在しなかったら } else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) { // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する - // (補間できないため) + // (隙間埋めできないため) const outdatedLog = await repository.findOne({ where: { date: LessThan(Chart.dateToTimestamp(gt)), @@ -690,7 +663,7 @@ export default abstract class Chart { if (log) { chart.unshift(this.convertRawRecord(log)); } else { - // 補間 + // 隙間埋め const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current)); const data = latest ? this.convertRawRecord(latest) : null; chart.unshift(this.getNewLog(data)); diff --git a/packages/backend/src/core/chart/entities.ts b/packages/backend/src/core/chart/entities.ts index e424f2c8c5..0f210041c1 100644 --- a/packages/backend/src/core/chart/entities.ts +++ b/packages/backend/src/core/chart/entities.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/deserializeAntenna.ts b/packages/backend/src/core/deserializeAntenna.ts deleted file mode 100644 index 1d0fbbdc86..0000000000 --- a/packages/backend/src/core/deserializeAntenna.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: noridev and cherrypick-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { MiAntenna } from '@/models/Antenna.js'; - -export function deserializeAntenna(body: any): MiAntenna { - return { - ...body, - lastUsedAt: new Date(body.lastUsedAt), - user: null, - userList: null, - }; -} diff --git a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts deleted file mode 100644 index 1e23c194c5..0000000000 --- a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { Packed } from '@/misc/json-schema.js'; -import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; - -@Injectable() -export class AbuseReportNotificationRecipientEntityService { - constructor( - @Inject(DI.abuseReportNotificationRecipientRepository) - private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, - private userEntityService: UserEntityService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - } - - @bindThis - public async pack( - src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient, - opts?: { - users: Map>, - webhooks: Map>, - }, - ): Promise> { - const recipient = typeof src === 'object' - ? src - : await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src }); - const user = recipient.userId - ? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId)) - : undefined; - const webhook = recipient.systemWebhookId - ? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId)) - : undefined; - - return { - id: recipient.id, - isActive: recipient.isActive, - updatedAt: recipient.updatedAt.toISOString(), - name: recipient.name, - method: recipient.method, - userId: recipient.userId ?? undefined, - user: user, - systemWebhookId: recipient.systemWebhookId ?? undefined, - systemWebhook: webhook, - }; - } - - @bindThis - public async packMany( - src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[], - ): Promise[]> { - const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object'); - const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string'); - if (ids.length > 0) { - objs.push( - ...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }), - ); - } - - const userIds = objs.map(it => it.userId).filter(x => x != null); - const users: Map> = (userIds.length > 0) - ? await this.userEntityService.packMany(userIds) - .then(it => new Map(it.map(it => [it.id, it]))) - : new Map(); - - const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null); - const systemWebhooks: Map> = (systemWebhookIds.length > 0) - ? await this.systemWebhookEntityService.packMany(systemWebhookIds) - .then(it => new Map(it.map(it => [it.id, it]))) - : new Map(); - - return Promise - .all( - objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })), - ) - .then(it => it.sort((a, b) => a.id.localeCompare(b.id))); - } -} - diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index a13c244c19..ee24b73097 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,6 @@ import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; -import type { Packed } from '@/misc/json-schema.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -27,11 +26,6 @@ export class AbuseUserReportEntityService { @bindThis public async pack( src: MiAbuseUserReport['id'] | MiAbuseUserReport, - hint?: { - packedReporter?: Packed<'UserDetailedNotMe'>, - packedTargetUser?: Packed<'UserDetailedNotMe'>, - packedAssignee?: Packed<'UserDetailedNotMe'>, - }, ) { const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src }); @@ -43,38 +37,23 @@ export class AbuseUserReportEntityService { reporterId: report.reporterId, targetUserId: report.targetUserId, assigneeId: report.assigneeId, - reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, { - schema: 'UserDetailedNotMe', + reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, { + detail: true, }), - targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, { - schema: 'UserDetailedNotMe', + targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, { + detail: true, }), - assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { - schema: 'UserDetailedNotMe', + assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { + detail: true, }) : null, forwarded: report.forwarded, }); } @bindThis - public async packMany( - reports: MiAbuseUserReport[], + public packMany( + reports: any[], ) { - const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId); - const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId); - const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null); - const _userMap = await this.userEntityService.packMany( - [..._reporters, ..._targetUsers, ..._assignees], - null, - { schema: 'UserDetailedNotMe' }, - ).then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - reports.map(report => { - const packedReporter = _userMap.get(report.reporterId); - const packedTargetUser = _userMap.get(report.targetUserId); - const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined; - return this.pack(report, { packedReporter, packedTargetUser, packedAssignee }); - }), - ); + return Promise.all(reports.map(x => this.pack(x))); } } diff --git a/packages/backend/src/core/entities/AnnouncementEntityService.ts b/packages/backend/src/core/entities/AnnouncementEntityService.ts deleted file mode 100644 index 90b04d0229..0000000000 --- a/packages/backend/src/core/entities/AnnouncementEntityService.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { AnnouncementsRepository, AnnouncementReadsRepository, MiAnnouncement, MiUser } from '@/models/_.js'; -import type { Packed } from '@/misc/json-schema.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; - -@Injectable() -export class AnnouncementEntityService { - constructor( - @Inject(DI.announcementsRepository) - private announcementsRepository: AnnouncementsRepository, - - @Inject(DI.announcementReadsRepository) - private announcementReadsRepository: AnnouncementReadsRepository, - - private idService: IdService, - ) { - } - - @bindThis - public async pack( - src: MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null }, - me?: { id: MiUser['id'] } | null | undefined, - ): Promise> { - const announcement = typeof src === 'object' - ? src - : await this.announcementsRepository.findOneByOrFail({ - id: src, - }) as MiAnnouncement & { isRead?: boolean | null }; - - if (me && announcement.isRead === undefined) { - announcement.isRead = await this.announcementReadsRepository - .countBy({ - announcementId: announcement.id, - userId: me.id, - }) - .then((count: number) => count > 0); - } - - return { - id: announcement.id, - createdAt: this.idService.parse(announcement.id).date.toISOString(), - updatedAt: announcement.updatedAt?.toISOString() ?? null, - title: announcement.title, - text: announcement.text, - imageUrl: announcement.imageUrl, - icon: announcement.icon, - display: announcement.display, - forYou: announcement.userId === me?.id, - needConfirmationToRead: announcement.needConfirmationToRead, - silence: announcement.silence, - isRead: announcement.isRead !== null ? announcement.isRead : undefined, - }; - } - - @bindThis - public async packMany( - announcements: (MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null } | MiAnnouncement)[], - me?: { id: MiUser['id'] } | null | undefined, - ) : Promise[]> { - return (await Promise.allSettled(announcements.map(x => this.pack(x, me)))) - .filter(result => result.status === 'fulfilled') - .map(result => (result as PromiseFulfilledResult>).value); - } -} diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 09fcb1eba6..f6b1847b8e 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -44,12 +44,11 @@ export class AntennaEntityService { users: antenna.users, caseSensitive: antenna.caseSensitive, localOnly: antenna.localOnly, - excludeBots: antenna.excludeBots, + notify: antenna.notify, withReplies: antenna.withReplies, withFile: antenna.withFile, isActive: antenna.isActive, hasUnreadNote: false, // TODO - notify: false, // 後方互換性のため }; } } diff --git a/packages/backend/src/core/entities/AppEntityService.ts b/packages/backend/src/core/entities/AppEntityService.ts index 785b84689a..875718e838 100644 --- a/packages/backend/src/core/entities/AppEntityService.ts +++ b/packages/backend/src/core/entities/AppEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/AuthSessionEntityService.ts b/packages/backend/src/core/entities/AuthSessionEntityService.ts index 72873680c9..7bb2a9e26e 100644 --- a/packages/backend/src/core/entities/AuthSessionEntityService.ts +++ b/packages/backend/src/core/entities/AuthSessionEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts index 1e699032e2..09a74298db 100644 --- a/packages/backend/src/core/entities/BlockingEntityService.ts +++ b/packages/backend/src/core/entities/BlockingEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -29,9 +29,6 @@ export class BlockingEntityService { public async pack( src: MiBlocking['id'] | MiBlocking, me?: { id: MiUser['id'] } | null | undefined, - hint?: { - blockee?: Packed<'UserDetailedNotMe'>, - }, ): Promise> { const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src }); @@ -39,20 +36,17 @@ export class BlockingEntityService { id: blocking.id, createdAt: this.idService.parse(blocking.id).date.toISOString(), blockeeId: blocking.blockeeId, - blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, { - schema: 'UserDetailedNotMe', + blockee: this.userEntityService.pack(blocking.blockeeId, me, { + detail: true, }), }); } @bindThis - public async packMany( - blockings: MiBlocking[], + public packMany( + blockings: any[], me: { id: MiUser['id'] }, ) { - const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId); - const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) }))); + return Promise.all(blockings.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 1ba7ca8e57..4ffd38dc69 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -51,14 +51,14 @@ export class ChannelEntityService { const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null; - const isFollowing = meId ? await this.channelFollowingsRepository.exists({ + const isFollowing = meId ? await this.channelFollowingsRepository.exist({ where: { followerId: meId, followeeId: channel.id, }, }) : false; - const isFavorited = meId ? await this.channelFavoritesRepository.exists({ + const isFavorited = meId ? await this.channelFavoritesRepository.exist({ where: { userId: meId, channelId: channel.id, diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts index d915645906..a38a2ece14 100644 --- a/packages/backend/src/core/entities/ClipEntityService.ts +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ClipNotesRepository, ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js'; +import type { ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/Blocking.js'; @@ -20,9 +20,6 @@ export class ClipEntityService { @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - @Inject(DI.clipFavoritesRepository) private clipFavoritesRepository: ClipFavoritesRepository, @@ -35,9 +32,6 @@ export class ClipEntityService { public async pack( src: MiClip['id'] | MiClip, me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedUser?: Packed<'UserLite'> - }, ): Promise> { const meId = me ? me.id : null; const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src }); @@ -47,25 +41,21 @@ export class ClipEntityService { createdAt: this.idService.parse(clip.id).date.toISOString(), lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null, userId: clip.userId, - user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId), + user: this.userEntityService.pack(clip.user ?? clip.userId), name: clip.name, description: clip.description, isPublic: clip.isPublic, favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), - isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined, - notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined, + isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined, }); } @bindThis - public async packMany( + public packMany( clips: MiClip[], me?: { id: MiUser['id'] } | null | undefined, ) { - const _users = clips.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(clips.map(clip => this.pack(clip, me, { packedUser: _userMap.get(clip.userId) }))); + return Promise.all(clips.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index de81a39fa8..8fe7e6c419 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -16,6 +16,7 @@ import { appendQuery, query } from '@/misc/prelude/url.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; import { UtilityService } from '../UtilityService.js'; import { VideoProcessingService } from '../VideoProcessingService.js'; @@ -109,18 +110,6 @@ export class DriveFileEntityService { @bindThis public getPublicUrl(file: MiDriveFile, mode?: 'avatar', ap?: boolean): string { // static = thumbnail - // PublicUrlにはexternalMediaProxyEnabledでもremoteProxyを使う - // https://github.com/yojo-art/cherrypick/issues/84 - if (file.uri != null && file.userHost != null && mode !== 'avatar' && this.config.remoteProxy != null) { - //下のローカルプロキシからコピペで持ってきた - const key = file.webpublicAccessKey; - if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 - if (this.config.remoteProxy.startsWith('/')) { - return `${this.config.url}${this.config.remoteProxy}/${key}`; - } - return `${this.config.remoteProxy}/${key}`; - } - } // リモートかつメディアプロキシ if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { return this.getProxiedUrl(file.uri, mode); @@ -243,9 +232,6 @@ export class DriveFileEntityService { public async packNullable( src: MiDriveFile['id'] | MiDriveFile, options?: PackOptions, - hint?: { - packedUser?: Packed<'UserLite'> - }, ): Promise | null> { const opts = Object.assign({ detail: false, @@ -272,8 +258,8 @@ export class DriveFileEntityService { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { detail: true, }) : null, - userId: file.userId, - user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null, + userId: opts.withUser ? file.userId : null, + user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null, }); } @@ -282,11 +268,8 @@ export class DriveFileEntityService { files: MiDriveFile[], options?: PackOptions, ): Promise[]> { - const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null); - const _userMap = await this.userEntityService.packMany(_user) - .then(users => new Map(users.map(user => [user.id, user]))); - const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {}))); - return items.filter(x => x != null); + const items = await Promise.all(files.map(f => this.packNullable(f, options))); + return items.filter((x): x is Packed<'DriveFile'> => x != null); } @bindThis @@ -311,6 +294,6 @@ export class DriveFileEntityService { ): Promise[]> { if (fileIds.length === 0) return []; const filesMap = await this.packManyByIdsMap(fileIds, options); - return fileIds.map(id => filesMap.get(id)).filter(x => x != null); + return fileIds.map(id => filesMap.get(id)).filter(isNotNull); } } diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts index 299f23ad38..05f66d989d 100644 --- a/packages/backend/src/core/entities/DriveFolderEntityService.ts +++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 841bd731c0..582a1606e1 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -31,7 +31,6 @@ export class EmojiEntityService { category: emoji.category, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url: emoji.publicUrl || emoji.originalUrl, - localOnly: emoji.localOnly ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, }; diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts index d110f7afc6..80ab25ccdd 100644 --- a/packages/backend/src/core/entities/FlashEntityService.ts +++ b/packages/backend/src/core/entities/FlashEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -33,9 +33,6 @@ export class FlashEntityService { public async pack( src: MiFlash['id'] | MiFlash, me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedUser?: Packed<'UserLite'> - }, ): Promise> { const meId = me ? me.id : null; const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src }); @@ -45,24 +42,21 @@ export class FlashEntityService { createdAt: this.idService.parse(flash.id).date.toISOString(), updatedAt: flash.updatedAt.toISOString(), userId: flash.userId, - user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意 + user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { detail: true } すると無限ループするので注意 title: flash.title, summary: flash.summary, script: flash.script, likedCount: flash.likedCount, - isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined, + isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined, }); } @bindThis - public async packMany( - flashes: MiFlash[], + public packMany( + flashs: MiFlash[], me?: { id: MiUser['id'] } | null | undefined, ) { - const _users = flashes.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) }))); + return Promise.all(flashs.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/FlashLikeEntityService.ts b/packages/backend/src/core/entities/FlashLikeEntityService.ts index 6e0b9d6e11..a3d86b4092 100644 --- a/packages/backend/src/core/entities/FlashLikeEntityService.ts +++ b/packages/backend/src/core/entities/FlashLikeEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts index 0101ec8aa7..8399df9ef6 100644 --- a/packages/backend/src/core/entities/FollowRequestEntityService.ts +++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,6 @@ import type { } from '@/models/Blocking.js'; import type { MiUser } from '@/models/User.js'; import type { MiFollowRequest } from '@/models/FollowRequest.js'; import { bindThis } from '@/decorators.js'; -import type { Packed } from '@/misc/json-schema.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -27,36 +26,14 @@ export class FollowRequestEntityService { public async pack( src: MiFollowRequest['id'] | MiFollowRequest, me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedFollower?: Packed<'UserLite'>, - packedFollowee?: Packed<'UserLite'>, - }, ) { const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src }); return { id: request.id, - follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me), - followee: hint?.packedFollowee ?? await this.userEntityService.pack(request.followeeId, me), + follower: await this.userEntityService.pack(request.followerId, me), + followee: await this.userEntityService.pack(request.followeeId, me), }; } - - @bindThis - public async packMany( - requests: MiFollowRequest[], - me?: { id: MiUser['id'] } | null | undefined, - ) { - const _followers = requests.map(({ follower, followerId }) => follower ?? followerId); - const _followees = requests.map(({ followee, followeeId }) => followee ?? followeeId); - const _userMap = await this.userEntityService.packMany([..._followers, ..._followees], me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - requests.map(req => { - const packedFollower = _userMap.get(req.followerId); - const packedFollowee = _userMap.get(req.followeeId); - return this.pack(req, me, { packedFollower, packedFollowee }); - }), - ); - } } diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts index d2dbaf2270..81121b72c9 100644 --- a/packages/backend/src/core/entities/FollowingEntityService.ts +++ b/packages/backend/src/core/entities/FollowingEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -78,10 +78,6 @@ export class FollowingEntityService { populateFollowee?: boolean; populateFollower?: boolean; }, - hint?: { - packedFollowee?: Packed<'UserDetailedNotMe'>, - packedFollower?: Packed<'UserDetailedNotMe'>, - }, ): Promise> { const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src }); @@ -92,35 +88,25 @@ export class FollowingEntityService { createdAt: this.idService.parse(following.id).date.toISOString(), followeeId: following.followeeId, followerId: following.followerId, - followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, { - schema: 'UserDetailedNotMe', + followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, { + detail: true, }) : undefined, - follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, { - schema: 'UserDetailedNotMe', + follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, { + detail: true, }) : undefined, }); } @bindThis - public async packMany( - followings: MiFollowing[], + public packMany( + followings: any[], me?: { id: MiUser['id'] } | null | undefined, opts?: { populateFollowee?: boolean; populateFollower?: boolean; }, ) { - const _followees = opts?.populateFollowee ? followings.map(({ followee, followeeId }) => followee ?? followeeId) : []; - const _followers = opts?.populateFollower ? followings.map(({ follower, followerId }) => follower ?? followerId) : []; - const _userMap = await this.userEntityService.packMany([..._followees, ..._followers], me, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - followings.map(following => { - const packedFollowee = opts?.populateFollowee ? _userMap.get(following.followeeId) : undefined; - const packedFollower = opts?.populateFollower ? _userMap.get(following.followerId) : undefined; - return this.pack(following, me, opts, { packedFollowee, packedFollower }); - }), - ); + return Promise.all(followings.map(x => this.pack(x, me, opts))); } } diff --git a/packages/backend/src/core/entities/GalleryLikeEntityService.ts b/packages/backend/src/core/entities/GalleryLikeEntityService.ts index f199a81b4d..62c95e119f 100644 --- a/packages/backend/src/core/entities/GalleryLikeEntityService.ts +++ b/packages/backend/src/core/entities/GalleryLikeEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts index 9746a4c1af..c1ea5ca43f 100644 --- a/packages/backend/src/core/entities/GalleryPostEntityService.ts +++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -35,9 +35,6 @@ export class GalleryPostEntityService { public async pack( src: MiGalleryPost['id'] | MiGalleryPost, me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedUser?: Packed<'UserLite'> - }, ): Promise> { const meId = me ? me.id : null; const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src }); @@ -47,7 +44,7 @@ export class GalleryPostEntityService { createdAt: this.idService.parse(post.id).date.toISOString(), updatedAt: post.updatedAt.toISOString(), userId: post.userId, - user: hint?.packedUser ?? this.userEntityService.pack(post.user ?? post.userId, me), + user: this.userEntityService.pack(post.user ?? post.userId, me), title: post.title, description: post.description, fileIds: post.fileIds, @@ -56,19 +53,16 @@ export class GalleryPostEntityService { tags: post.tags.length > 0 ? post.tags : undefined, isSensitive: post.isSensitive, likedCount: post.likedCount, - isLiked: meId ? await this.galleryLikesRepository.exists({ where: { postId: post.id, userId: meId } }) : undefined, + isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined, }); } @bindThis - public async packMany( + public packMany( posts: MiGalleryPost[], me?: { id: MiUser['id'] } | null | undefined, ) { - const _users = posts.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(posts.map(post => this.pack(post, me, { packedUser: _userMap.get(post.userId) }))); + return Promise.all(posts.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/HashtagEntityService.ts b/packages/backend/src/core/entities/HashtagEntityService.ts index d798b15807..04ebac9e89 100644 --- a/packages/backend/src/core/entities/HashtagEntityService.ts +++ b/packages/backend/src/core/entities/HashtagEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 4c45c13167..3bb5936818 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,15 +8,12 @@ import type { Packed } from '@/misc/json-schema.js'; import type { MiInstance } from '@/models/Instance.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { MiUser } from '@/models/User.js'; +import { UtilityService } from '../UtilityService.js'; @Injectable() export class InstanceEntityService { constructor( private metaService: MetaService, - private roleService: RoleService, private utilityService: UtilityService, ) { @@ -25,11 +22,8 @@ export class InstanceEntityService { @bindThis public async pack( instance: MiInstance, - me?: { id: MiUser['id']; } | null | undefined, ): Promise> { const meta = await this.metaService.fetch(); - const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; - return { id: instance.id, firstRetrievedAt: instance.firstRetrievedAt.toISOString(), @@ -39,8 +33,7 @@ export class InstanceEntityService { followingCount: instance.followingCount, followersCount: instance.followersCount, isNotResponding: instance.isNotResponding, - isSuspended: instance.suspensionState !== 'none', - suspensionState: instance.suspensionState, + isSuspended: instance.isSuspended, isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, @@ -50,13 +43,11 @@ export class InstanceEntityService { maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), - isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host), iconUrl: instance.iconUrl, faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, - moderationNote: iAmModerator ? instance.moderationNote : null, }; } diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts index 5d3e823a2a..2d0b8be09a 100644 --- a/packages/backend/src/core/entities/InviteCodeEntityService.ts +++ b/packages/backend/src/core/entities/InviteCodeEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -29,10 +29,6 @@ export class InviteCodeEntityService { public async pack( src: MiRegistrationTicket['id'] | MiRegistrationTicket, me?: { id: MiUser['id'] } | null | undefined, - hints?: { - packedCreatedBy?: Packed<'UserLite'>, - packedUsedBy?: Packed<'UserLite'>, - }, ): Promise> { const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({ where: { @@ -46,28 +42,18 @@ export class InviteCodeEntityService { code: target.code, expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null, createdAt: this.idService.parse(target.id).date.toISOString(), - createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null, - usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null, + createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null, + usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null, usedAt: target.usedAt ? target.usedAt.toISOString() : null, used: !!target.usedAt, }); } @bindThis - public async packMany( - tickets: MiRegistrationTicket[], + public packMany( + targets: any[], me: { id: MiUser['id'] }, ) { - const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null); - const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null); - const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all( - tickets.map(ticket => { - const packedCreatedBy = ticket.createdById != null ? _userMap.get(ticket.createdById) : undefined; - const packedUsedBy = ticket.usedById != null ? _userMap.get(ticket.usedById) : undefined; - return this.pack(ticket, me, { packedCreatedBy, packedUsedBy }); - }), - ); + return Promise.all(targets.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/MessagingMessageEntityService.ts b/packages/backend/src/core/entities/MessagingMessageEntityService.ts index 40d961bacb..5974e984f8 100644 --- a/packages/backend/src/core/entities/MessagingMessageEntityService.ts +++ b/packages/backend/src/core/entities/MessagingMessageEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts deleted file mode 100644 index 5ca17ff78e..0000000000 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Brackets } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; -import JSON5 from 'json5'; -import type { Packed } from '@/misc/json-schema.js'; -import type { MiMeta } from '@/models/Meta.js'; -import type { AdsRepository } from '@/models/_.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { MetaService } from '@/core/MetaService.js'; -import { bindThis } from '@/decorators.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; -import type { Config } from '@/config.js'; -import { DI } from '@/di-symbols.js'; -import { DEFAULT_POLICIES } from '@/core/RoleService.js'; - -@Injectable() -export class MetaEntityService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.adsRepository) - private adsRepository: AdsRepository, - - private userEntityService: UserEntityService, - private metaService: MetaService, - private instanceActorService: InstanceActorService, - ) { } - - @bindThis - public async pack(meta?: MiMeta): Promise> { - let instance = meta; - - if (!instance) { - instance = await this.metaService.fetch(); - } - - const ads = await this.adsRepository.createQueryBuilder('ads') - .where('ads.expiresAt > :now', { now: new Date() }) - .andWhere('ads.startsAt <= :now', { now: new Date() }) - .andWhere(new Brackets(qb => { - // 曜日のビットフラグを確認する - qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() }) - .orWhere('ads.dayOfWeek = 0'); - })) - .getMany(); - - // クライアントの手間を減らすためあらかじめJSONに変換しておく - let defaultLightTheme = null; - let defaultDarkTheme = null; - if (instance.defaultLightTheme) { - try { - defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme)); - } catch (e) { - } - } - if (instance.defaultDarkTheme) { - try { - defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme)); - } catch (e) { - } - } - - const packed: Packed<'MetaLite'> = { - maintainerName: instance.maintainerName, - maintainerEmail: instance.maintainerEmail, - - version: this.config.version, - basedMisskeyVersion: this.config.basedMisskeyVersion, - providesTarball: this.config.publishTarballInsteadOfProvideRepositoryUrl, - - name: instance.name, - shortName: instance.shortName, - uri: this.config.url, - description: instance.description, - langs: instance.langs, - tosUrl: instance.termsOfServiceUrl, - repositoryUrl: instance.repositoryUrl, - feedbackUrl: instance.feedbackUrl, - impressumUrl: instance.impressumUrl, - privacyPolicyUrl: instance.privacyPolicyUrl, - inquiryUrl: instance.inquiryUrl, - disableRegistration: instance.disableRegistration, - emailRequiredForSignup: instance.emailRequiredForSignup, - enableHcaptcha: instance.enableHcaptcha, - hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableMcaptcha: instance.enableMcaptcha, - mcaptchaSiteKey: instance.mcaptchaSitekey, - mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, - enableRecaptcha: instance.enableRecaptcha, - recaptchaSiteKey: instance.recaptchaSiteKey, - enableTurnstile: instance.enableTurnstile, - turnstileSiteKey: instance.turnstileSiteKey, - swPublickey: instance.swPublicKey, - themeColor: instance.themeColor, - mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png', - bannerUrl: instance.bannerUrl, - infoImageUrl: instance.infoImageUrl, - serverErrorImageUrl: instance.serverErrorImageUrl, - notFoundImageUrl: instance.notFoundImageUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, - defaultLightTheme, - defaultDarkTheme, - ads: ads.map(ad => ({ - id: ad.id, - url: ad.url, - place: ad.place, - ratio: ad.ratio, - imageUrl: ad.imageUrl, - dayOfWeek: ad.dayOfWeek, - })), - notesPerOneAd: instance.notesPerOneAd, - enableEmail: instance.enableEmail, - enableServiceWorker: instance.enableServiceWorker, - - translatorAvailable: instance.deeplAuthKey != null, - - serverRules: instance.serverRules, - - policies: { ...DEFAULT_POLICIES, ...instance.policies }, - - mediaProxy: this.config.mediaProxy, - enableUrlPreview: instance.urlPreviewEnabled, - urlPreviewEndpoint: instance.directSummalyProxy ? (instance.urlPreviewSummaryProxyUrl || `${this.config.url}/url` ) : `${this.config.url}/url`, - noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local', - }; - - return packed; - } - - @bindThis - public async packDetailed(meta?: MiMeta): Promise> { - let instance = meta; - - if (!instance) { - instance = await this.metaService.fetch(); - } - - const packed = await this.pack(instance); - - const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null; - - const packDetailed: Packed<'MetaDetailed'> = { - ...packed, - cacheRemoteFiles: instance.cacheRemoteFiles, - cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, - requireSetup: !await this.instanceActorService.realLocalUsersPresent(), - proxyAccountName: proxyAccount ? proxyAccount.username : null, - features: { - localTimeline: instance.policies.ltlAvailable, - globalTimeline: instance.policies.gtlAvailable, - registration: !instance.disableRegistration, - emailRequiredForSignup: instance.emailRequiredForSignup, - hcaptcha: instance.enableHcaptcha, - recaptcha: instance.enableRecaptcha, - turnstile: instance.enableTurnstile, - objectStorage: instance.useObjectStorage, - serviceWorker: instance.enableServiceWorker, - miauth: true, - }, - }; - - return packDetailed; - } -} - diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts index bf1b2a002c..6973e64c19 100644 --- a/packages/backend/src/core/entities/ModerationLogEntityService.ts +++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,10 +8,9 @@ import { DI } from '@/di-symbols.js'; import type { ModerationLogsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { } from '@/models/Blocking.js'; -import { MiModerationLog } from '@/models/ModerationLog.js'; +import type { MiModerationLog } from '@/models/ModerationLog.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; -import type { Packed } from '@/misc/json-schema.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -28,9 +27,6 @@ export class ModerationLogEntityService { @bindThis public async pack( src: MiModerationLog['id'] | MiModerationLog, - hint?: { - packedUser?: Packed<'UserDetailedNotMe'>, - }, ) { const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src }); @@ -40,20 +36,17 @@ export class ModerationLogEntityService { type: log.type, info: log.info, userId: log.userId, - user: hint?.packedUser ?? this.userEntityService.pack(log.user ?? log.userId, null, { - schema: 'UserDetailedNotMe', + user: this.userEntityService.pack(log.user ?? log.userId, null, { + detail: true, }), }); } @bindThis - public async packMany( - reports: MiModerationLog[], + public packMany( + reports: any[], ) { - const _users = reports.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, null, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(reports.map(report => this.pack(report, { packedUser: _userMap.get(report.userId) }))); + return Promise.all(reports.map(x => this.pack(x))); } } diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts index d361a20271..ef035a7091 100644 --- a/packages/backend/src/core/entities/MutingEntityService.ts +++ b/packages/backend/src/core/entities/MutingEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -30,9 +30,6 @@ export class MutingEntityService { public async pack( src: MiMuting['id'] | MiMuting, me?: { id: MiUser['id'] } | null | undefined, - hints?: { - packedMutee?: Packed<'UserDetailedNotMe'>, - }, ): Promise> { const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src }); @@ -41,21 +38,18 @@ export class MutingEntityService { createdAt: this.idService.parse(muting.id).date.toISOString(), expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null, muteeId: muting.muteeId, - mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, { - schema: 'UserDetailedNotMe', + mutee: this.userEntityService.pack(muting.muteeId, me, { + detail: true, }), }); } @bindThis - public async packMany( - mutings: MiMuting[], + public packMany( + mutings: any[], me: { id: MiUser['id'] }, ) { - const _mutees = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId); - const _userMap = await this.userEntityService.packMany(_mutees, me, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) }))); + return Promise.all(mutings.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index aa25e46807..fba1b098ed 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,6 +14,7 @@ import type { MiNote } from '@/models/Note.js'; import type { MiNoteReaction } from '@/models/NoteReaction.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, EventsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -96,11 +97,6 @@ export class NoteEntityService implements OnModuleInit { } } - // visibilityがprivateかつ自分が投稿者でなかったら非表示 - if (packedNote.visibility === 'private' && meId !== packedNote.userId) { - hide = true; - } - // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 if (packedNote.visibility === 'followers') { if (meId == null) { @@ -115,7 +111,7 @@ export class NoteEntityService implements OnModuleInit { hide = false; } else { // フォロワーかどうか - const isFollowing = await this.followingsRepository.exists({ + const isFollowing = await this.followingsRepository.exist({ where: { followeeId: packedNote.userId, followerId: meId, @@ -171,7 +167,7 @@ export class NoteEntityService implements OnModuleInit { return { multiple: poll.multiple, - expiresAt: poll.expiresAt?.toISOString() ?? null, + expiresAt: poll.expiresAt, choices, }; } @@ -243,13 +239,6 @@ export class NoteEntityService implements OnModuleInit { } } - // visibilityがspecifiedかつ自分が指定されていなかったら非表示 - if (note.visibility === 'private') { - if (meId !== note.userId) { - return false; - } - } - // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 if (note.visibility === 'followers') { if (meId == null) { @@ -301,7 +290,7 @@ export class NoteEntityService implements OnModuleInit { packedFiles.set(k, v); } } - return fileIds.map(id => packedFiles.get(id)).filter(x => x != null); + return fileIds.map(id => packedFiles.get(id)).filter(isNotNull); } @bindThis @@ -315,7 +304,6 @@ export class NoteEntityService implements OnModuleInit { _hint_?: { myReactions: Map; packedFiles: Map | null>; - packedUsers: Map> }; }, ): Promise> { @@ -346,7 +334,6 @@ export class NoteEntityService implements OnModuleInit { .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis([note])); const packedFiles = options?._hint_?.packedFiles; - const packedUsers = options?._hint_?.packedUsers; const packed: Packed<'Note'> = await awaitAll({ id: note.id, @@ -355,7 +342,9 @@ export class NoteEntityService implements OnModuleInit { updatedAtHistory: note.updatedAtHistory ? note.updatedAtHistory.map(x => x.toISOString()) : undefined, noteEditHistory: note.noteEditHistory.length ? note.noteEditHistory : undefined, userId: note.userId, - user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me), + user: this.userEntityService.pack(note.user ?? note.userId, me, { + detail: false, + }), text: text, cw: note.cw, visibility: note.visibility, @@ -365,7 +354,6 @@ export class NoteEntityService implements OnModuleInit { disableRightClick: note.disableRightClick || undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, - reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0), reactions: this.reactionService.convertLegacyReactions(note.reactions), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined, @@ -382,7 +370,6 @@ export class NoteEntityService implements OnModuleInit { color: channel.color, isSensitive: channel.isSensitive, allowRenoteToExternal: channel.allowRenoteToExternal, - userId: channel.userId, } : undefined, mentions: note.mentions.length > 0 ? note.mentions : undefined, uri: note.uri ?? undefined, @@ -407,7 +394,6 @@ export class NoteEntityService implements OnModuleInit { poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, event: note.hasEvent ? this.populateEvent(note) : undefined, - deleteAt: note.deleteAt?.toISOString() ?? undefined, ...(meId && Object.keys(note.reactions).length > 0 ? { myReaction: this.populateMyReaction(note, meId, options?._hint_), @@ -481,22 +467,14 @@ export class NoteEntityService implements OnModuleInit { await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)); // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく - const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null); + const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull); const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map(); - const users = [ - ...notes.map(({ user, userId }) => user ?? userId), - ...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null), - ...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null), - ]; - const packedUsers = await this.userEntityService.packMany(users, me) - .then(users => new Map(users.map(u => [u.id, u]))); return await Promise.all(notes.map(n => this.pack(n, me, { ...options, _hint_: { myReactions: myReactionsMap, packedFiles, - packedUsers, }, }))); } diff --git a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts index 3cdafe48ad..b8a5aaf8ae 100644 --- a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts +++ b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index 46ec13704c..46c0897822 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -52,9 +52,6 @@ export class NoteReactionEntityService implements OnModuleInit { options?: { withNote: boolean; }, - hints?: { - packedUser?: Packed<'UserLite'> - }, ): Promise> { const opts = Object.assign({ withNote: false, @@ -65,28 +62,11 @@ export class NoteReactionEntityService implements OnModuleInit { return { id: reaction.id, createdAt: this.idService.parse(reaction.id).date.toISOString(), - user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me), + user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me), type: this.reactionService.convertLegacyReaction(reaction.reaction), ...(opts.withNote ? { note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me), } : {}), }; } - - @bindThis - public async packMany( - reactions: MiNoteReaction[], - me?: { id: MiUser['id'] } | null | undefined, - options?: { - withNote: boolean; - }, - ): Promise[]> { - const opts = Object.assign({ - withNote: false, - }, options); - const _users = reactions.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) }))); - } } diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 625d65f60b..da51797579 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,15 +13,16 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio import type { MiNote } from '@/models/Note.js'; import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; -import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js'; -import { CacheService } from '@/core/CacheService.js'; +import { isNotNull } from '@/misc/is-not-null.js'; +import { FilterUnionByProperty, notificationTypes } from '@/types.js'; import { RoleEntityService } from './RoleEntityService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); +const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']); @Injectable() export class NotificationEntityService implements OnModuleInit { @@ -45,8 +46,6 @@ export class NotificationEntityService implements OnModuleInit { @Inject(DI.userGroupInvitationsRepository) private userGroupInvitationsRepository: UserGroupInvitationsRepository, - private cacheService: CacheService, - //private userEntityService: UserEntityService, //private noteEntityService: NoteEntityService, //private userGroupInvitationEntityService: UserGroupInvitationEntityService, @@ -60,61 +59,163 @@ export class NotificationEntityService implements OnModuleInit { this.userGroupInvitationEntityService = this.moduleRef.get('UserGroupInvitationEntityService'); } - /** - * 通知をパックする共通処理 - */ - async #packInternal ( - src: T, + @bindThis + public async pack( + src: MiNotification, meId: MiUser['id'], // eslint-disable-next-line @typescript-eslint/ban-types options: { - checkValidNotifier?: boolean; + }, hint?: { packedNotes: Map>; - packedUsers: Map>; + packedUsers: Map>; }, - ): Promise | null> { + ): Promise> { const notification = src; + const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? ( + hint?.packedNotes != null + ? hint.packedNotes.get(notification.noteId) + : this.noteEntityService.pack(notification.noteId, { id: meId }, { + detail: true, + }) + ) : undefined; + const userIfNeed = 'notifierId' in notification ? ( + hint?.packedUsers != null + ? hint.packedUsers.get(notification.notifierId) + : this.userEntityService.pack(notification.notifierId, { id: meId }, { + detail: false, + }) + ) : undefined; + const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined; + + return await awaitAll({ + id: notification.id, + createdAt: new Date(notification.createdAt).toISOString(), + type: notification.type, + userId: 'notifierId' in notification ? notification.notifierId : undefined, + ...(userIfNeed != null ? { user: userIfNeed } : {}), + ...(noteIfNeed != null ? { note: noteIfNeed } : {}), + ...(notification.type === 'reaction' ? { + reaction: notification.reaction, + } : {}), + ...(notification.type === 'roleAssigned' ? { + role: role, + } : {}), + // ...(notification.type === 'pollEnded' ? { + // note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + // detail: true, + // _hint_: options._hintForEachNotes_, + // }), + // } : {}), + ...(notification.type === 'groupInvited' ? { + invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId), + } : {}), + ...(notification.type === 'achievementEarned' ? { + achievement: notification.achievement, + } : {}), + ...(notification.type === 'app' ? { + body: notification.customBody, + header: notification.customHeader, + icon: notification.customIcon, + } : {}), + }); + } - if (options.checkValidNotifier !== false && !(await this.#isValidNotifier(notification, meId))) return null; + @bindThis + public async packMany( + notifications: MiNotification[], + meId: MiUser['id'], + ) { + if (notifications.length === 0) return []; + + let validNotifications = notifications; + + const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull); + const notes = noteIds.length > 0 ? await this.notesRepository.find({ + where: { id: In(noteIds) }, + relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'], + }) : []; + const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, { + detail: true, + }); + const packedNotes = new Map(packedNotesArray.map(p => [p.id, p])); + + validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId)); + + const userIds = validNotifications.map(x => 'notifierId' in x ? x.notifierId : null).filter(isNotNull); + const users = userIds.length > 0 ? await this.usersRepository.find({ + where: { id: In(userIds) }, + }) : []; + const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, { + detail: false, + }); + const packedUsers = new Map(packedUsersArray.map(p => [p.id, p])); + + // 既に解決されたフォローリクエストの通知を除外 + const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty => x.type === 'receiveFollowRequest'); + if (followRequestNotifications.length > 0) { + const reqs = await this.followRequestsRepository.find({ + where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) }, + }); + validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId)); + } - const needsNote = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification; - const noteIfNeed = needsNote ? ( + const groupInvitedNotifications = validNotifications.filter((x): x is FilterUnionByProperty => x.type === 'groupInvited'); + if (groupInvitedNotifications.length > 0) { + const existingInvitationIds = await this.userGroupInvitationsRepository.find({ + where: { id: In(groupInvitedNotifications.map(x => x.userGroupInvitationId)) }, + }); + validNotifications = validNotifications.filter(x => (x.type !== 'groupInvited') || existingInvitationIds.some(r => r.id === x.userGroupInvitationId)); + } + + return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, { + packedNotes, + packedUsers, + }))); + } + + @bindThis + public async packGrouped( + src: MiGroupedNotification, + meId: MiUser['id'], + // eslint-disable-next-line @typescript-eslint/ban-types + options: { + + }, + hint?: { + packedNotes: Map>; + packedUsers: Map>; + }, + ): Promise> { + const notification = src; + const noteIfNeed = NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? ( hint?.packedNotes != null ? hint.packedNotes.get(notification.noteId) : this.noteEntityService.pack(notification.noteId, { id: meId }, { detail: true, }) ) : undefined; - // if the note has been deleted, don't show this notification - if (needsNote && !noteIfNeed) return null; - - const needsUser = 'notifierId' in notification; - const userIfNeed = needsUser ? ( + const userIfNeed = 'notifierId' in notification ? ( hint?.packedUsers != null ? hint.packedUsers.get(notification.notifierId) - : this.userEntityService.pack(notification.notifierId, { id: meId }) + : this.userEntityService.pack(notification.notifierId, { id: meId }, { + detail: false, + }) ) : undefined; - // if the user has been deleted, don't show this notification - if (needsUser && !userIfNeed) return null; - // #region Grouped notifications if (notification.type === 'reaction:grouped') { - const reactions = (await Promise.all(notification.reactions.map(async reaction => { + const reactions = await Promise.all(notification.reactions.map(async reaction => { const user = hint?.packedUsers != null ? hint.packedUsers.get(reaction.userId)! - : await this.userEntityService.pack(reaction.userId, { id: meId }); + : await this.userEntityService.pack(reaction.userId, { id: meId }, { + detail: false, + }); return { user, reaction: reaction.reaction, }; - }))).filter(r => r.user != null); - // if all users have been deleted, don't show this notification - if (reactions.length === 0) { - return null; - } - + })); return await awaitAll({ id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), @@ -123,19 +224,16 @@ export class NotificationEntityService implements OnModuleInit { reactions, }); } else if (notification.type === 'renote:grouped') { - const users = (await Promise.all(notification.userIds.map(userId => { + const users = await Promise.all(notification.userIds.map(userId => { const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null; if (packedUser) { return packedUser; } - return this.userEntityService.pack(userId, { id: meId }); - }))).filter(x => x != null); - // if all users have been deleted, don't show this notification - if (users.length === 0) { - return null; - } - + return this.userEntityService.pack(userId, { id: meId }, { + detail: false, + }); + })); return await awaitAll({ id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), @@ -144,14 +242,8 @@ export class NotificationEntityService implements OnModuleInit { users, }); } - // #endregion - const needsRole = notification.type === 'roleAssigned'; - const role = needsRole ? await this.roleEntityService.pack(notification.roleId) : undefined; - // if the role has been deleted, don't show this notification - if (needsRole && !role) { - return null; - } + const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined; return await awaitAll({ id: notification.id, @@ -163,12 +255,12 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'reaction' ? { reaction: notification.reaction, } : {}), - ...(notification.type === 'groupInvited' ? { - invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId), - } : {}), ...(notification.type === 'roleAssigned' ? { role: role, } : {}), + ...(notification.type === 'groupInvited' ? { + invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId), + } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), @@ -180,17 +272,16 @@ export class NotificationEntityService implements OnModuleInit { }); } - async #packManyInternal ( - notifications: T[], + @bindThis + public async packGroupedMany( + notifications: MiGroupedNotification[], meId: MiUser['id'], - ): Promise { + ) { if (notifications.length === 0) return []; let validNotifications = notifications; - validNotifications = await this.#filterValidNotifier(validNotifications, meId); - - const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null); + const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull); const notes = noteIds.length > 0 ? await this.notesRepository.find({ where: { id: In(noteIds) }, relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'], @@ -211,11 +302,13 @@ export class NotificationEntityService implements OnModuleInit { const users = userIds.length > 0 ? await this.usersRepository.find({ where: { id: In(userIds) }, }) : []; - const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }); + const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, { + detail: false, + }); const packedUsers = new Map(packedUsersArray.map(p => [p.id, p])); // 既に解決されたフォローリクエストの通知を除外 - const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty => x.type === 'receiveFollowRequest'); + const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty => x.type === 'receiveFollowRequest'); if (followRequestNotifications.length > 0) { const reqs = await this.followRequestsRepository.find({ where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) }, @@ -223,107 +316,17 @@ export class NotificationEntityService implements OnModuleInit { validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId)); } - const packPromises = validNotifications.map(x => { - return this.pack( - x, - meId, - { checkValidNotifier: false }, - { packedNotes, packedUsers }, - ); - }); - - return (await Promise.all(packPromises)).filter(x => x != null); - } - - @bindThis - public async pack( - src: MiNotification | MiGroupedNotification, - meId: MiUser['id'], - // eslint-disable-next-line @typescript-eslint/ban-types - options: { - checkValidNotifier?: boolean; - }, - hint?: { - packedNotes: Map>; - packedUsers: Map>; - }, - ): Promise | null> { - return await this.#packInternal(src, meId, options, hint); - } - - @bindThis - public async packMany( - notifications: MiNotification[], - meId: MiUser['id'], - ): Promise { - return await this.#packManyInternal(notifications, meId); - } - - @bindThis - public async packGroupedMany( - notifications: MiGroupedNotification[], - meId: MiUser['id'], - ): Promise { - return await this.#packManyInternal(notifications, meId); - } - - /** - * notifierが存在するか、ミュートされていないか、サスペンドされていないかを確認するvalidator - */ - #validateNotifier ( - notification: T, - userIdsWhoMeMuting: Set, - userMutedInstances: Set, - notifiers: MiUser[], - ): boolean { - if (!('notifierId' in notification)) return true; - if (userIdsWhoMeMuting.has(notification.notifierId)) return false; - - const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null; - - if (notifier == null) return false; - if (notifier.host && userMutedInstances.has(notifier.host)) return false; - - if (notifier.isSuspended) return false; - - return true; - } - - /** - * notifierが存在するか、ミュートされていないか、サスペンドされていないかを実際に確認する - */ - async #isValidNotifier( - notification: MiNotification | MiGroupedNotification, - meId: MiUser['id'], - ): Promise { - return (await this.#filterValidNotifier([notification], meId)).length === 1; - } - - /** - * notifierが存在するか、ミュートされていないか、サスペンドされていないかを実際に複数確認する - */ - async #filterValidNotifier ( - notifications: T[], - meId: MiUser['id'], - ): Promise { - const [ - userIdsWhoMeMuting, - userMutedInstances, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(meId), - this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)), - ]); - - const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null); - const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({ - where: { id: In(notifierIds) }, - }) : []; - - const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => { - const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers); - return isValid ? notification : null; - }))) as [T | null] ).filter(x => x != null); + const groupInvitedNotifications = validNotifications.filter((x): x is FilterUnionByProperty => x.type === 'groupInvited'); + if (groupInvitedNotifications.length > 0) { + const existingInvitationIds = await this.userGroupInvitationsRepository.find({ + where: { id: In(groupInvitedNotifications.map(x => x.userGroupInvitationId)) }, + }); + validNotifications = validNotifications.filter(x => (x.type !== 'groupInvited') || existingInvitationIds.some(r => r.id === x.userGroupInvitationId)); + } - return filteredNotifications; + return await Promise.all(validNotifications.map(x => this.packGrouped(x, meId, {}, { + packedNotes, + packedUsers, + }))); } } diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts index 46bf51bb6d..17d4c0b575 100644 --- a/packages/backend/src/core/entities/PageEntityService.ts +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -39,9 +39,6 @@ export class PageEntityService { public async pack( src: MiPage['id'] | MiPage, me?: { id: MiUser['id'] } | null | undefined, - hint?: { - packedUser?: Packed<'UserLite'> - }, ): Promise> { const meId = me ? me.id : null; const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src }); @@ -93,7 +90,7 @@ export class PageEntityService { createdAt: this.idService.parse(page.id).date.toISOString(), updatedAt: page.updatedAt.toISOString(), userId: page.userId, - user: hint?.packedUser ?? this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意 + user: this.userEntityService.pack(page.user ?? page.userId, me), // { detail: true } すると無限ループするので注意 content: page.content, variables: page.variables, title: page.title, @@ -105,21 +102,18 @@ export class PageEntityService { script: page.script, eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, - attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)), + attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)), likedCount: page.likedCount, - isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, + isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined, }); } @bindThis - public async packMany( + public packMany( pages: MiPage[], me?: { id: MiUser['id'] } | null | undefined, ) { - const _users = pages.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(pages.map(page => this.pack(page, me, { packedUser: _userMap.get(page.userId) }))); + return Promise.all(pages.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/PageLikeEntityService.ts b/packages/backend/src/core/entities/PageLikeEntityService.ts index cfccbcb660..29c1c08392 100644 --- a/packages/backend/src/core/entities/PageLikeEntityService.ts +++ b/packages/backend/src/core/entities/PageLikeEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts index e4e154109a..6ecb8f7671 100644 --- a/packages/backend/src/core/entities/RenoteMutingEntityService.ts +++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -30,9 +30,6 @@ export class RenoteMutingEntityService { public async pack( src: MiRenoteMuting['id'] | MiRenoteMuting, me?: { id: MiUser['id'] } | null | undefined, - hints?: { - packedMutee?: Packed<'UserDetailedNotMe'> - }, ): Promise> { const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src }); @@ -40,21 +37,18 @@ export class RenoteMutingEntityService { id: muting.id, createdAt: this.idService.parse(muting.id).date.toISOString(), muteeId: muting.muteeId, - mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, { - schema: 'UserDetailedNotMe', + mutee: this.userEntityService.pack(muting.muteeId, me, { + detail: true, }), }); } @bindThis - public async packMany( - mutings: MiRenoteMuting[], + public packMany( + mutings: any[], me: { id: MiUser['id'] }, ) { - const _users = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId); - const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailedNotMe' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) }))); + return Promise.all(mutings.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 2a7dc37bce..c5d9c0dccd 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/SigninEntityService.ts b/packages/backend/src/core/entities/SigninEntityService.ts index 00b124d594..a9f6260777 100644 --- a/packages/backend/src/core/entities/SigninEntityService.ts +++ b/packages/backend/src/core/entities/SigninEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/SystemWebhookEntityService.ts b/packages/backend/src/core/entities/SystemWebhookEntityService.ts deleted file mode 100644 index e18734091c..0000000000 --- a/packages/backend/src/core/entities/SystemWebhookEntityService.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { Packed } from '@/misc/json-schema.js'; - -@Injectable() -export class SystemWebhookEntityService { - constructor( - @Inject(DI.systemWebhooksRepository) - private systemWebhooksRepository: SystemWebhooksRepository, - ) { - } - - @bindThis - public async pack( - src: MiSystemWebhook['id'] | MiSystemWebhook, - opts?: { - webhooks: Map - }, - ): Promise> { - const webhook = typeof src === 'object' - ? src - : opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src }); - - return { - id: webhook.id, - isActive: webhook.isActive, - updatedAt: webhook.updatedAt.toISOString(), - latestSentAt: webhook.latestSentAt?.toISOString() ?? null, - latestStatus: webhook.latestStatus, - name: webhook.name, - on: webhook.on, - url: webhook.url, - secret: webhook.secret, - }; - } - - @bindThis - public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise[]> { - if (src.length === 0) { - return []; - } - - const webhooks = Array.of(); - webhooks.push( - ...src.filter((it): it is MiSystemWebhook => typeof it === 'object'), - ); - - const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string'); - if (ids.length > 0) { - webhooks.push( - ...await this.systemWebhooksRepository.findBy({ id: In(ids) }), - ); - } - - return Promise - .all( - webhooks.map(x => - this.pack(x, { - webhooks: new Map(webhooks.map(x => [x.id, x])), - }), - ), - ) - .then(it => it.sort((a, b) => a.id.localeCompare(b.id))); - } -} - diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 4b2f5a0ef0..d4d3676ca3 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,32 +15,8 @@ import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import { - birthdaySchema, - descriptionSchema, - localUsernameSchema, - locationSchema, - nameSchema, - passwordSchema, -} from '@/models/User.js'; -import type { - BlockingsRepository, - FollowingsRepository, - FollowRequestsRepository, - MessagingMessagesRepository, - MiFollowing, - MiUserNotePining, - MiUserProfile, - MutingsRepository, - NoteUnreadsRepository, - RenoteMutingsRepository, - UserGroupJoiningsRepository, - UserMemoRepository, - UserNotePiningsRepository, - UserProfilesRepository, - UserSecurityKeysRepository, - UsersRepository, -} from '@/models/_.js'; +import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js'; +import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; @@ -54,6 +30,14 @@ import type { NoteEntityService } from './NoteEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; +type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; +type IsMeAndIsUserDetailed = + Detailed extends true ? + ExpectsMe extends true ? Packed<'MeDetailed'> : + ExpectsMe extends false ? Packed<'UserDetailedNotMe'> : + Packed<'UserDetailed'> : + Packed<'UserLite'>; + const Ajv = _Ajv.default; const ajv = new Ajv(); @@ -69,23 +53,11 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean { return !isLocalUser(user); } -export type UserRelation = { - id: MiUser['id'] - following: MiFollowing | null, - isFollowing: boolean - isFollowed: boolean - hasPendingFollowRequestFromYou: boolean - hasPendingFollowRequestToYou: boolean - isBlocking: boolean - isBlocked: boolean - isMuted: boolean - isRenoteMuted: boolean -} - @Injectable() export class UserEntityService implements OnModuleInit { private apPersonService: ApPersonService; private noteEntityService: NoteEntityService; + private driveFileEntityService: DriveFileEntityService; private pageEntityService: PageEntityService; private customEmojiService: CustomEmojiService; private announcementService: AnnouncementService; @@ -124,6 +96,9 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + @Inject(DI.noteUnreadsRepository) private noteUnreadsRepository: NoteUnreadsRepository, @@ -139,6 +114,12 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.userGroupJoiningsRepository) private userGroupJoiningsRepository: UserGroupJoiningsRepository, + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, + + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + @Inject(DI.userMemosRepository) private userMemosRepository: UserMemoRepository, ) { @@ -147,6 +128,7 @@ export class UserEntityService implements OnModuleInit { onModuleInit() { this.apPersonService = this.moduleRef.get('ApPersonService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); + this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.pageEntityService = this.moduleRef.get('PageEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.announcementService = this.moduleRef.get('AnnouncementService'); @@ -169,7 +151,7 @@ export class UserEntityService implements OnModuleInit { public isRemoteUser = isRemoteUser; @bindThis - public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise { + public async getRelation(me: MiUser['id'], target: MiUser['id']) { const [ following, isFollowed, @@ -184,43 +166,43 @@ export class UserEntityService implements OnModuleInit { followerId: me, followeeId: target, }), - this.followingsRepository.exists({ + this.followingsRepository.exist({ where: { followerId: target, followeeId: me, }, }), - this.followRequestsRepository.exists({ + this.followRequestsRepository.exist({ where: { followerId: me, followeeId: target, }, }), - this.followRequestsRepository.exists({ + this.followRequestsRepository.exist({ where: { followerId: target, followeeId: me, }, }), - this.blockingsRepository.exists({ + this.blockingsRepository.exist({ where: { blockerId: me, blockeeId: target, }, }), - this.blockingsRepository.exists({ + this.blockingsRepository.exist({ where: { blockerId: target, blockeeId: me, }, }), - this.mutingsRepository.exists({ + this.mutingsRepository.exist({ where: { muterId: me, muteeId: target, }, }), - this.renoteMutingsRepository.exists({ + this.renoteMutingsRepository.exist({ where: { muterId: me, muteeId: target, @@ -242,80 +224,6 @@ export class UserEntityService implements OnModuleInit { }; } - @bindThis - public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise> { - const [ - followers, - followees, - followersRequests, - followeesRequests, - blockers, - blockees, - muters, - renoteMuters, - ] = await Promise.all([ - this.followingsRepository.findBy({ followerId: me }) - .then(f => new Map(f.map(it => [it.followeeId, it]))), - this.followingsRepository.createQueryBuilder('f') - .select('f.followerId') - .where('f.followeeId = :me', { me }) - .getRawMany<{ f_followerId: string }>() - .then(it => it.map(it => it.f_followerId)), - this.followRequestsRepository.createQueryBuilder('f') - .select('f.followeeId') - .where('f.followerId = :me', { me }) - .getRawMany<{ f_followeeId: string }>() - .then(it => it.map(it => it.f_followeeId)), - this.followRequestsRepository.createQueryBuilder('f') - .select('f.followerId') - .where('f.followeeId = :me', { me }) - .getRawMany<{ f_followerId: string }>() - .then(it => it.map(it => it.f_followerId)), - this.blockingsRepository.createQueryBuilder('b') - .select('b.blockeeId') - .where('b.blockerId = :me', { me }) - .getRawMany<{ b_blockeeId: string }>() - .then(it => it.map(it => it.b_blockeeId)), - this.blockingsRepository.createQueryBuilder('b') - .select('b.blockerId') - .where('b.blockeeId = :me', { me }) - .getRawMany<{ b_blockerId: string }>() - .then(it => it.map(it => it.b_blockerId)), - this.mutingsRepository.createQueryBuilder('m') - .select('m.muteeId') - .where('m.muterId = :me', { me }) - .getRawMany<{ m_muteeId: string }>() - .then(it => it.map(it => it.m_muteeId)), - this.renoteMutingsRepository.createQueryBuilder('m') - .select('m.muteeId') - .where('m.muterId = :me', { me }) - .getRawMany<{ m_muteeId: string }>() - .then(it => it.map(it => it.m_muteeId)), - ]); - - return new Map( - targets.map(target => { - const following = followers.get(target) ?? null; - - return [ - target, - { - id: target, - following: following, - isFollowing: following != null, - isFollowed: followees.includes(target), - hasPendingFollowRequestFromYou: followersRequests.includes(target), - hasPendingFollowRequestToYou: followeesRequests.includes(target), - isBlocking: blockers.includes(target), - isBlocked: blockees.includes(target), - isMuted: muters.includes(target), - isRenoteMuted: renoteMuters.includes(target), - }, - ]; - }), - ); - } - @bindThis public async getHasUnreadMessagingMessage(userId: MiUser['id']): Promise { const mute = await this.mutingsRepository.findBy({ @@ -351,7 +259,7 @@ export class UserEntityService implements OnModuleInit { /* const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); - const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exists({ + const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({ where: { antennaId: In(myAntennas.map(x => x.id)), read: false, @@ -431,65 +339,33 @@ export class UserEntityService implements OnModuleInit { return `${this.config.url}/users/${userId}`; } - public async pack( + public async pack( src: MiUser['id'] | MiUser, me?: { id: MiUser['id']; } | null | undefined, options?: { - schema?: S, + detail?: D, includeSecrets?: boolean, userProfile?: MiUserProfile, - userRelations?: Map, - userMemos?: Map, - pinNotes?: Map, }, - ): Promise> { + ): Promise> { const opts = Object.assign({ - schema: 'UserLite', + detail: false, includeSecrets: false, }, options); const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src }); - const isDetailed = opts.schema !== 'UserLite'; const meId = me ? me.id : null; const isMe = meId === user.id; const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; - const profile = isDetailed - ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) - : null; - - let relation: UserRelation | null = null; - if (meId && !isMe && isDetailed) { - if (opts.userRelations) { - relation = opts.userRelations.get(user.id) ?? null; - } else { - relation = await this.getRelation(meId, user.id); - } - } - - let memo: string | null = null; - if (isDetailed && meId) { - if (opts.userMemos) { - memo = opts.userMemos.get(user.id) ?? null; - } else { - memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id }) - .then(row => row?.memo ?? null); - } - } - - let pins: MiUserNotePining[] = []; - if (isDetailed) { - if (opts.pinNotes) { - pins = opts.pinNotes.get(user.id) ?? []; - } else { - pins = await this.userNotePiningsRepository.createQueryBuilder('pin') - .where('pin.userId = :userId', { userId: user.id }) - .innerJoinAndSelect('pin.note', 'note') - .orderBy('pin.id', 'DESC') - .getMany(); - } - } + const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null; + const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin') + .where('pin.userId = :userId', { userId: user.id }) + .innerJoinAndSelect('pin.note', 'note') + .orderBy('pin.id', 'DESC') + .getMany() : []; + const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; const followingCount = profile == null ? null : (profile.followingVisibility === 'public') || isMe ? user.followingCount : @@ -501,15 +377,15 @@ export class UserEntityService implements OnModuleInit { (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; - const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null; - const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null; - const unreadAnnouncements = isMe && isDetailed ? + const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; + const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null; + const unreadAnnouncements = isMe && opts.detail ? (await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({ createdAt: this.idService.parse(announcement.id).date.toISOString(), ...announcement, })) : null; - const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null; + const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null; const packed = { id: user.id, @@ -530,8 +406,6 @@ export class UserEntityService implements OnModuleInit { }))) : [], isBot: user.isBot, isCat: user.isCat, - isIndexable: user.isIndexable, - isSensitive: user.isSensitive, instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { name: instance.name, softwareName: instance.softwareName, @@ -543,23 +417,19 @@ export class UserEntityService implements OnModuleInit { emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), // パフォーマンス上の理由でローカルユーザーのみ - badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs - .filter((r) => r.isPublic || iAmModerator) - .sort((a, b) => b.displayOrder - a.displayOrder) - .map((r) => ({ - name: r.name, - iconUrl: r.iconUrl, - displayOrder: r.displayOrder, - })) - ) : undefined, - - ...(isDetailed ? { + badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({ + name: r.name, + iconUrl: r.iconUrl, + displayOrder: r.displayOrder, + }))) : undefined, + + ...(opts.detail ? { url: profile!.url, uri: user.uri, movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, alsoKnownAs: user.alsoKnownAs ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) - .then(xs => xs.length === 0 ? null : xs.filter(x => x != null)) + .then(xs => xs.length === 0 ? null : xs.filter(x => x != null) as string[]) : null, createdAt: this.idService.parse(user.id).date.toISOString(), updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, @@ -569,7 +439,6 @@ export class UserEntityService implements OnModuleInit { isLocked: user.isLocked, isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), isSuspended: user.isSuspended, - isSensitive: user.isSensitive, description: profile!.description, location: profile!.location, birthday: profile!.birthday, @@ -585,13 +454,15 @@ export class UserEntityService implements OnModuleInit { }), pinnedPageId: profile!.pinnedPageId, pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, - publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 + publicReactions: profile!.publicReactions, followersVisibility: profile!.followersVisibility, followingVisibility: profile!.followingVisibility, twoFactorEnabled: profile!.twoFactorEnabled, usePasswordLessLogin: profile!.usePasswordLessLogin, securityKeys: profile!.twoFactorEnabled - ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1) + ? this.userSecurityKeysRepository.countBy({ + userId: user.id, + }).then(result => result >= 1) : false, roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ id: role.id, @@ -603,11 +474,14 @@ export class UserEntityService implements OnModuleInit { isAdministrator: role.isAdministrator, displayOrder: role.displayOrder, }))), - memo: memo, + memo: meId == null ? null : await this.userMemosRepository.findOneBy({ + userId: meId, + targetUserId: user.id, + }).then(row => row?.memo ?? null), moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, } : {}), - ...(isDetailed && isMe ? { + ...(opts.detail && isMe ? { avatarId: user.avatarId, bannerId: user.bannerId, isModerator: isModerator, @@ -680,82 +554,19 @@ export class UserEntityService implements OnModuleInit { notify: relation.following?.notify ?? 'none', withReplies: relation.following?.withReplies ?? false, } : {}), - } as Promiseable>; + } as Promiseable> as Promiseable>; return await awaitAll(packed); } - public async packMany( + public packMany( users: (MiUser['id'] | MiUser)[], me?: { id: MiUser['id'] } | null | undefined, options?: { - schema?: S, + detail?: D, includeSecrets?: boolean, }, - ): Promise[]> { - // -- IDのみの要素を補完して完全なエンティティ一覧を作る - - const _users = users.filter((user): user is MiUser => typeof user !== 'string'); - if (_users.length !== users.length) { - _users.push( - ...await this.usersRepository.findBy({ - id: In(users.filter((user): user is string => typeof user === 'string')), - }), - ); - } - const _userIds = _users.map(u => u.id); - - // -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得 - - let profilesMap: Map = new Map(); - let userRelations: Map = new Map(); - let userMemos: Map = new Map(); - let pinNotes: Map = new Map(); - - if (options?.schema !== 'UserLite') { - profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) }) - .then(profiles => new Map(profiles.map(p => [p.userId, p]))); - - const meId = me ? me.id : null; - if (meId) { - userMemos = await this.userMemosRepository.findBy({ userId: meId }) - .then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo]))); - - if (_userIds.length > 0) { - userRelations = await this.getRelations(meId, _userIds); - pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin') - .where('pin.userId IN (:...userIds)', { userIds: _userIds }) - .innerJoinAndSelect('pin.note', 'note') - .getMany() - .then(pinsNotes => { - const map = new Map(); - for (const note of pinsNotes) { - const notes = map.get(note.userId) ?? []; - notes.push(note); - map.set(note.userId, notes); - } - for (const [, notes] of map.entries()) { - // pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく - notes.sort((a, b) => b.id.localeCompare(a.id)); - } - return map; - }); - } - } - } - - return Promise.all( - _users.map(u => this.pack( - u, - me, - { - ...options, - userProfile: profilesMap.get(u.id), - userRelations: userRelations, - userMemos: userMemos, - pinNotes: pinNotes, - }, - )), - ); + ): Promise[]> { + return Promise.all(users.map(u => this.pack(u, me, options))); } } diff --git a/packages/backend/src/core/entities/UserGroupEntityService.ts b/packages/backend/src/core/entities/UserGroupEntityService.ts index 402d27923c..72de937062 100644 --- a/packages/backend/src/core/entities/UserGroupEntityService.ts +++ b/packages/backend/src/core/entities/UserGroupEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts index 9500d6c95f..ba1f30ae9d 100644 --- a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts +++ b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index b77249c5cb..3026684689 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -50,14 +50,11 @@ export class UserListEntityService { public async packMembershipsMany( memberships: MiUserListMembership[], ) { - const _users = memberships.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users) - .then(users => new Map(users.map(u => [u.id, u]))); return Promise.all(memberships.map(async x => ({ id: x.id, createdAt: this.idService.parse(x.id).date.toISOString(), userId: x.userId, - user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId), + user: await this.userEntityService.pack(x.userId), withReplies: x.withReplies, }))); } diff --git a/packages/backend/src/daemons/DaemonModule.ts b/packages/backend/src/daemons/DaemonModule.ts index a67907e6dd..b9afbcff47 100644 --- a/packages/backend/src/daemons/DaemonModule.ts +++ b/packages/backend/src/daemons/DaemonModule.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts index fb32416a2d..b839cb36c7 100644 --- a/packages/backend/src/daemons/QueueStatsService.ts +++ b/packages/backend/src/daemons/QueueStatsService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index 2c70344c94..a8b337c479 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown { const log = [] as any[]; ev.on('requestServerStatsLog', x => { - ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length)); + ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50)); }); const tick = async () => { diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts index 21777657d1..25a1eb6365 100644 --- a/packages/backend/src/decorators.ts +++ b/packages/backend/src/decorators.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index a88c695958..5854945a6f 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,6 @@ export const DI = { config: Symbol('config'), db: Symbol('db'), meilisearch: Symbol('meilisearch'), - opensearch: Symbol('opensearch'), cloudLogging: Symbol('cloudLogging'), redis: Symbol('redis'), redisForPub: Symbol('redisForPub'), @@ -57,7 +56,6 @@ export const DI = { swSubscriptionsRepository: Symbol('swSubscriptionsRepository'), hashtagsRepository: Symbol('hashtagsRepository'), abuseUserReportsRepository: Symbol('abuseUserReportsRepository'), - abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'), registrationTicketsRepository: Symbol('registrationTicketsRepository'), authSessionsRepository: Symbol('authSessionsRepository'), accessTokensRepository: Symbol('accessTokensRepository'), @@ -80,7 +78,6 @@ export const DI = { channelFavoritesRepository: Symbol('channelFavoritesRepository'), registryItemsRepository: Symbol('registryItemsRepository'), webhooksRepository: Symbol('webhooksRepository'), - systemWebhooksRepository: Symbol('systemWebhooksRepository'), adsRepository: Symbol('adsRepository'), passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'), retentionAggregationsRepository: Symbol('retentionAggregationsRepository'), @@ -89,6 +86,5 @@ export const DI = { flashsRepository: Symbol('flashsRepository'), flashLikesRepository: Symbol('flashLikesRepository'), userMemosRepository: Symbol('userMemosRepository'), - bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), //#endregion }; diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index ba44cfa2e6..615c09037b 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/global.d.ts b/packages/backend/src/global.d.ts index 2f19e85525..8d1a10077e 100644 --- a/packages/backend/src/global.d.ts +++ b/packages/backend/src/global.d.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index 2d0627d367..21d9b053e3 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -25,29 +25,33 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; export default class Logger { private context: Context; private parentLogger: Logger | null = null; + private store: boolean; private cloudLogging: Log | null | undefined; - constructor(context: string, color?: KEYWORD, cloudLogging?: Log) { + constructor(context: string, color?: KEYWORD, store = true, cloudLogging?: Log) { this.context = { name: context, color: color, }; + this.store = store; this.cloudLogging = cloudLogging; } @bindThis - public createSubLogger(context: string, color?: KEYWORD): Logger { - const logger = new Logger(context, color); + public createSubLogger(context: string, color?: KEYWORD, store = true): Logger { + const logger = new Logger(context, color, store); logger.parentLogger = this; return logger; } @bindThis - private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = []): void { + private log(level: Level, message: string, data?: Record | null, important = false, subContexts: Context[] = [], store = true): void { if (envOption.quiet) return; + if (!this.store) store = false; + if (level === 'debug') store = false; if (this.parentLogger) { - this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts)); + this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store); return; } @@ -73,11 +77,8 @@ export default class Logger { let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; - const args: unknown[] = [important ? chalk.bold(log) : log]; - if (data != null) { - args.push(data); - } - console.log(...args); + console.log(important ? chalk.bold(log) : log); + if (level === 'error' && data) console.log(data); this.writeCloudLogging(level, log, timestamp, level === 'error' || level === 'warning' ? data : null); } diff --git a/packages/backend/src/misc/FileWriterStream.ts b/packages/backend/src/misc/FileWriterStream.ts deleted file mode 100644 index 367a8eb560..0000000000 --- a/packages/backend/src/misc/FileWriterStream.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as fs from 'node:fs/promises'; -import type { PathLike } from 'node:fs'; - -/** - * `fs.createWriteStream()`相当のことを行う`WritableStream` (Web標準) - */ -export class FileWriterStream extends WritableStream { - constructor(path: PathLike) { - let file: fs.FileHandle | null = null; - - super({ - start: async () => { - file = await fs.open(path, 'a'); - }, - write: async (chunk, controller) => { - if (file === null) { - controller.error(); - throw new Error(); - } - - await file.write(chunk); - }, - close: async () => { - await file?.close(); - }, - abort: async () => { - await file?.close(); - }, - }); - } -} diff --git a/packages/backend/src/misc/JsonArrayStream.ts b/packages/backend/src/misc/JsonArrayStream.ts deleted file mode 100644 index 754938989d..0000000000 --- a/packages/backend/src/misc/JsonArrayStream.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { TransformStream } from 'node:stream/web'; - -/** - * ストリームに流れてきた各データについて`JSON.stringify()`した上で、それらを一つの配列にまとめる - */ -export class JsonArrayStream extends TransformStream { - constructor() { - /** 最初の要素かどうかを変数に記録 */ - let isFirst = true; - - super({ - start(controller) { - controller.enqueue('['); - }, - flush(controller) { - controller.enqueue(']'); - }, - transform(chunk, controller) { - if (isFirst) { - isFirst = false; - } else { - // 妥当なJSON配列にするためには最初以外の要素の前に`,`を挿入しなければならない - controller.enqueue(',\n'); - } - - controller.enqueue(JSON.stringify(chunk)); - }, - }); - } -} diff --git a/packages/backend/src/misc/acct.ts b/packages/backend/src/misc/acct.ts index 3d729b1151..8aa62426fe 100644 --- a/packages/backend/src/misc/acct.ts +++ b/packages/backend/src/misc/acct.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 71f7b838a8..746d3ad198 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -192,18 +192,28 @@ export class RedisSingleCache { // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? -export class MemoryKVCache { - /** - * データを持つマップ - * @deprecated これを直接操作するべきではない - */ - public cache: Map; +function nothingToDo(value: T): V { + return value as unknown as V; +} + +export class MemoryKVCache { + public cache: Map; private lifetime: number; private gcIntervalHandle: NodeJS.Timeout; - - constructor(lifetime: MemoryKVCache['lifetime']) { + private toMapConverter: (value: T) => V; + private fromMapConverter: (cached: V) => T | undefined; + + constructor(lifetime: MemoryKVCache['lifetime'], options: { + toMapConverter: (value: T) => V; + fromMapConverter: (cached: V) => T | undefined; + } = { + toMapConverter: nothingToDo, + fromMapConverter: nothingToDo, + }) { this.cache = new Map(); this.lifetime = lifetime; + this.toMapConverter = options.toMapConverter; + this.fromMapConverter = options.fromMapConverter; this.gcIntervalHandle = setInterval(() => { this.gc(); @@ -211,14 +221,10 @@ export class MemoryKVCache { } @bindThis - /** - * Mapにキャッシュをセットします - * @deprecated これを直接呼び出すべきではない。InternalEventなどで変更を全てのプロセス/マシンに通知するべき - */ public set(key: string, value: T): void { this.cache.set(key, { date: Date.now(), - value, + value: this.toMapConverter(value), }); } @@ -230,7 +236,7 @@ export class MemoryKVCache { this.cache.delete(key); return undefined; } - return cached.value; + return this.fromMapConverter(cached.value); } @bindThis @@ -241,9 +247,10 @@ export class MemoryKVCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetch(key: string, fetcher: (value: V | undefined) => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -258,7 +265,7 @@ export class MemoryKVCache { } // Cache MISS - const value = await fetcher(); + const value = await fetcher(this.cache.get(key)?.value); this.set(key, value); return value; } @@ -266,9 +273,10 @@ export class MemoryKVCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetchMaybe(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -283,7 +291,7 @@ export class MemoryKVCache { } // Cache MISS - const value = await fetcher(); + const value = await fetcher(this.cache.get(key)?.value); if (value !== undefined) { this.set(key, value); } diff --git a/packages/backend/src/misc/check-https.ts b/packages/backend/src/misc/check-https.ts index 15a54f6ce7..065f1562bb 100644 --- a/packages/backend/src/misc/check-https.ts +++ b/packages/backend/src/misc/check-https.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts index c50f2b723c..baee4adbdc 100644 --- a/packages/backend/src/misc/check-word-mute.ts +++ b/packages/backend/src/misc/check-word-mute.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/clone.ts b/packages/backend/src/misc/clone.ts index ed05485649..f4379be578 100644 --- a/packages/backend/src/misc/clone.ts +++ b/packages/backend/src/misc/clone.ts @@ -1,12 +1,12 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ // structredCloneが遅いため // SEE: http://var.blog.jp/archives/86038606.html -type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[]; +type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; export function deepClone(x: T): T { if (typeof x === 'object') { @@ -14,7 +14,7 @@ export function deepClone(x: T): T { if (Array.isArray(x)) return x.map(deepClone) as T; const obj = {} as Record; for (const [k, v] of Object.entries(x)) { - obj[k] = v === undefined ? undefined : deepClone(v); + obj[k] = deepClone(v); } return obj as T; } else { diff --git a/packages/backend/src/misc/content-disposition.ts b/packages/backend/src/misc/content-disposition.ts index 467b5057d6..b88dbe22b0 100644 --- a/packages/backend/src/misc/content-disposition.ts +++ b/packages/backend/src/misc/content-disposition.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/correct-filename.ts b/packages/backend/src/misc/correct-filename.ts index f7ee02781d..27effa7752 100644 --- a/packages/backend/src/misc/correct-filename.ts +++ b/packages/backend/src/misc/correct-filename.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -21,7 +21,7 @@ const extRegExp = /\.[0-9a-zA-Z]+$/i; /** * 与えられた拡張子とファイル名が一致しているかどうかを確認し、 * 一致していない場合は拡張子を付与して返す - * + * * extはfile-typeのextを想定 */ export function correctFilename(filename: string, ext: string | null) { diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index 9aaecf8263..a282f46c20 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/dev-null.ts b/packages/backend/src/misc/dev-null.ts index 4d9806fbe8..cb79d949e7 100644 --- a/packages/backend/src/misc/dev-null.ts +++ b/packages/backend/src/misc/dev-null.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts index 6d03b433ba..63151ef5d8 100644 --- a/packages/backend/src/misc/emoji-regex.ts +++ b/packages/backend/src/misc/emoji-regex.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts index 17b9dafd1e..21379c83fd 100644 --- a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts +++ b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts index 288c4a4c9e..631f64a5d4 100644 --- a/packages/backend/src/misc/extract-hashtags.ts +++ b/packages/backend/src/misc/extract-hashtags.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts index 3c3c60b898..febaa34a4a 100644 --- a/packages/backend/src/misc/extract-mentions.ts +++ b/packages/backend/src/misc/extract-mentions.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts deleted file mode 100644 index 3e1c099e00..0000000000 --- a/packages/backend/src/misc/fastify-hook-handlers.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { onRequestHookHandler } from 'fastify'; - -export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => { - const index = request.url.indexOf('?'); - if (~index) { - reply.redirect(301, request.url.slice(0, index)); - } - done(); -}; diff --git a/packages/backend/src/misc/fastify-reply-error.ts b/packages/backend/src/misc/fastify-reply-error.ts index e6c4e78d2f..cc1609c15c 100644 --- a/packages/backend/src/misc/fastify-reply-error.ts +++ b/packages/backend/src/misc/fastify-reply-error.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/flash-token.ts b/packages/backend/src/misc/flash-token.ts index 769501b60c..4c4791a887 100644 --- a/packages/backend/src/misc/flash-token.ts +++ b/packages/backend/src/misc/flash-token.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts index 342e0f8602..5f889a4d2e 100644 --- a/packages/backend/src/misc/gen-identicon.ts +++ b/packages/backend/src/misc/gen-identicon.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,8 +8,9 @@ * https://en.wikipedia.org/wiki/Identicon */ -import { createCanvas } from '@napi-rs/canvas'; +import * as p from 'pureimage'; import gen from 'random-seed'; +import type { WriteStream } from 'node:fs'; const size = 128; // px const n = 5; // resolution @@ -44,9 +45,9 @@ const sideN = Math.floor(n / 2); /** * Generate buffer of an identicon by seed */ -export async function genIdenticon(seed: string): Promise { +export function genIdenticon(seed: string, stream: WriteStream): Promise { const rand = gen.create(seed); - const canvas = createCanvas(size, size); + const canvas = p.make(size, size, undefined); const ctx = canvas.getContext('2d'); const bgColors = colors[rand(colors.length)]; @@ -100,5 +101,5 @@ export async function genIdenticon(seed: string): Promise { } } - return await canvas.encode('png'); + return p.encodePNGToStream(canvas, stream); } diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 02a303dc0a..9cefd6912e 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/generate-invite-code.ts b/packages/backend/src/misc/generate-invite-code.ts index 006920cf0e..91b7c66a1a 100644 --- a/packages/backend/src/misc/generate-invite-code.ts +++ b/packages/backend/src/misc/generate-invite-code.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/generate-native-user-token.ts b/packages/backend/src/misc/generate-native-user-token.ts index 85fb383ba2..18570b78c5 100644 --- a/packages/backend/src/misc/generate-native-user-token.ts +++ b/packages/backend/src/misc/generate-native-user-token.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts index e132fa8f31..9f3f3bf252 100644 --- a/packages/backend/src/misc/get-ip-hash.ts +++ b/packages/backend/src/misc/get-ip-hash.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index 1a07139a50..55d05642a1 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/get-reaction-emoji.ts b/packages/backend/src/misc/get-reaction-emoji.ts index 3f975853ed..6c4844eaf8 100644 --- a/packages/backend/src/misc/get-reaction-emoji.ts +++ b/packages/backend/src/misc/get-reaction-emoji.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts index 6cbbdef74c..bad65967b6 100644 --- a/packages/backend/src/misc/i18n.ts +++ b/packages/backend/src/misc/i18n.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index 60ba788e44..bf32dd1d2b 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/id/aidx.ts b/packages/backend/src/misc/id/aidx.ts index 1b087e70af..41186062f4 100644 --- a/packages/backend/src/misc/id/aidx.ts +++ b/packages/backend/src/misc/id/aidx.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts index dfab48a369..e4d1366869 100644 --- a/packages/backend/src/misc/id/meid.ts +++ b/packages/backend/src/misc/id/meid.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts index b9c0cc3dda..a028631586 100644 --- a/packages/backend/src/misc/id/meidg.ts +++ b/packages/backend/src/misc/id/meidg.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts index 243f92bbac..ef6dc2e719 100644 --- a/packages/backend/src/misc/id/object-id.ts +++ b/packages/backend/src/misc/id/object-id.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts index fc3654d6d2..56ddf846c5 100644 --- a/packages/backend/src/misc/id/ulid.ts +++ b/packages/backend/src/misc/id/ulid.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts index 13c41f1e3b..4c62c28b7d 100644 --- a/packages/backend/src/misc/identifiable-error.ts +++ b/packages/backend/src/misc/identifiable-error.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/is-duplicate-key-value-error.ts b/packages/backend/src/misc/is-duplicate-key-value-error.ts index 8da0280f60..5234b75da2 100644 --- a/packages/backend/src/misc/is-duplicate-key-value-error.ts +++ b/packages/backend/src/misc/is-duplicate-key-value-error.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/is-instance-muted.ts b/packages/backend/src/misc/is-instance-muted.ts index 096a8b39c7..b0c9dd508b 100644 --- a/packages/backend/src/misc/is-instance-muted.ts +++ b/packages/backend/src/misc/is-instance-muted.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/is-mime-image.ts b/packages/backend/src/misc/is-mime-image.ts index 8ffbc99230..c88a53a466 100644 --- a/packages/backend/src/misc/is-mime-image.ts +++ b/packages/backend/src/misc/is-mime-image.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/is-native-token.ts b/packages/backend/src/misc/is-native-token.ts index 300c4c05b3..cd3badfcb6 100644 --- a/packages/backend/src/misc/is-native-token.ts +++ b/packages/backend/src/misc/is-native-token.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts new file mode 100644 index 0000000000..8a9ec5ea5a --- /dev/null +++ b/packages/backend/src/misc/is-not-null.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// we are using {} as "any non-nullish value" as expected +// eslint-disable-next-line @typescript-eslint/ban-types +export function isNotNull(input: T | undefined | null): input is T { + return input != null; +} diff --git a/packages/backend/src/misc/is-pure-renote.ts b/packages/backend/src/misc/is-pure-renote.ts new file mode 100644 index 0000000000..994d981522 --- /dev/null +++ b/packages/backend/src/misc/is-pure-renote.ts @@ -0,0 +1,10 @@ +import type { MiNote } from '@/models/Note.js'; + +export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable } { + if (!note.renoteId) return false; + + if (note.text) return false; // it's quoted with text + if (note.fileIds.length !== 0) return false; // it's quoted with files + if (note.hasPoll) return false; // it's quoted with poll + return true; +} diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts new file mode 100644 index 0000000000..d53d7a6be8 --- /dev/null +++ b/packages/backend/src/misc/is-quote.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { MiNote } from '@/models/Note.js'; + +// eslint-disable-next-line import/no-default-export +export default function(note: MiNote): boolean { + // sync with NoteCreateService.isQuote + return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); +} diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts deleted file mode 100644 index 48f821806c..0000000000 --- a/packages/backend/src/misc/is-renote.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { MiNote } from '@/models/Note.js'; -import type { Packed } from '@/misc/json-schema.js'; - -type Renote = - MiNote & { - renoteId: NonNullable - }; - -type Quote = - Renote & ({ - text: NonNullable - } | { - cw: NonNullable - } | { - replyId: NonNullable - reply: NonNullable - } | { - hasPoll: true - }); - -export function isRenote(note: MiNote): note is Renote { - return note.renoteId != null; -} - -export function isQuote(note: Renote): note is Quote { - // NOTE: SYNC WITH NoteCreateService.isQuote - return note.text != null || - note.cw != null || - note.replyId != null || - note.hasPoll || - note.fileIds.length > 0; -} - -type PackedRenote = - Packed<'Note'> & { - renoteId: NonNullable['renoteId']> - }; - -type PackedQuote = - PackedRenote & ({ - text: NonNullable['text']> - } | { - cw: NonNullable['cw']> - } | { - replyId: NonNullable['replyId']> - } | { - poll: NonNullable['poll']> - } | { - fileIds: NonNullable['fileIds']> - }); - -export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote { - return note.renoteId != null; -} - -export function isQuotePacked(note: PackedRenote): note is PackedQuote { - return note.text != null || - note.cw != null || - note.replyId != null || - note.poll != null || - (note.fileIds != null && note.fileIds.length > 0); -} diff --git a/packages/backend/src/misc/is-reply.ts b/packages/backend/src/misc/is-reply.ts index 980eae11c9..42076cb7b7 100644 --- a/packages/backend/src/misc/is-reply.ts +++ b/packages/backend/src/misc/is-reply.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts index 862d6e6a38..1288689dd8 100644 --- a/packages/backend/src/misc/is-user-related.ts +++ b/packages/backend/src/misc/is-user-related.ts @@ -1,13 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ export function isUserRelated(note: any, userIds: Set, ignoreAuthor = false): boolean { - if (!note) { - return false; - } - if (userIds.has(note.userId) && !ignoreAuthor) { return true; } diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 9f35cd0c0f..5441b1cc46 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -1,15 +1,15 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { - packedMeDetailedOnlySchema, - packedMeDetailedSchema, + packedUserLiteSchema, packedUserDetailedNotMeOnlySchema, + packedMeDetailedOnlySchema, packedUserDetailedNotMeSchema, + packedMeDetailedSchema, packedUserDetailedSchema, - packedUserLiteSchema, packedUserSchema, } from '@/models/json-schema/user.js'; import { packedNoteSchema } from '@/models/json-schema/note.js'; @@ -26,7 +26,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js'; -import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js'; +import { packedPageSchema } from '@/models/json-schema/page.js'; import { packedUserGroupSchema } from '@/models/json-schema/user-group.js'; import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'; import { packedChannelSchema } from '@/models/json-schema/channel.js'; @@ -39,27 +39,8 @@ import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/jso import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; -import { - packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, - packedRoleCondFormulaLogicsSchema, - packedRoleCondFormulaValueAssignedRoleSchema, - packedRoleCondFormulaValueCreatedSchema, - packedRoleCondFormulaValueIsLocalOrRemoteSchema, - packedRoleCondFormulaValueNot, - packedRoleCondFormulaValueSchema, - packedRoleCondFormulaValueUserSettingBooleanSchema, - packedRoleLiteSchema, - packedRolePoliciesSchema, - packedRoleSchema, -} from '@/models/json-schema/role.js'; +import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; import { packedAdSchema } from '@/models/json-schema/ad.js'; -import { - packedMetaDetailedOnlySchema, - packedMetaDetailedSchema, - packedMetaLiteSchema, -} from '@/models/json-schema/meta.js'; -import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; -import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -89,7 +70,6 @@ export const refs = { Hashtag: packedHashtagSchema, InviteCode: packedInviteCodeSchema, Page: packedPageSchema, - PageBlock: packedPageBlockSchema, Channel: packedChannelSchema, QueueCount: packedQueueCountSchema, Antenna: packedAntennaSchema, @@ -100,29 +80,12 @@ export const refs = { EmojiDetailed: packedEmojiDetailedSchema, Flash: packedFlashSchema, Signin: packedSigninSchema, - RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema, - RoleCondFormulaValueNot: packedRoleCondFormulaValueNot, - RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema, - RoleCondFormulaValueUserSettingBooleanSchema: packedRoleCondFormulaValueUserSettingBooleanSchema, - RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema, - RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema, - RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, - RoleCondFormulaValue: packedRoleCondFormulaValueSchema, RoleLite: packedRoleLiteSchema, Role: packedRoleSchema, - RolePolicies: packedRolePoliciesSchema, - MetaLite: packedMetaLiteSchema, - MetaDetailedOnly: packedMetaDetailedOnlySchema, - MetaDetailed: packedMetaDetailedSchema, - SystemWebhook: packedSystemWebhookSchema, - AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, }; export type Packed = SchemaType; -export type KeyOf = PropertiesToUnion; -type PropertiesToUnion

= p['properties'] extends NonNullable ? keyof p['properties'] : never; - type TypeStringef = 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object' | 'any'; type StringDefToType = T extends 'null' ? null : @@ -152,7 +115,6 @@ export interface Schema extends OfSchema { readonly example?: any; readonly format?: string; readonly ref?: keyof typeof refs; - readonly selfRef?: boolean; readonly enum?: ReadonlyArray; readonly default?: (this['type'] extends TypeStringef ? StringDefToType : any) | null; readonly maxLength?: number; @@ -233,7 +195,7 @@ export type SchemaTypeDef

= p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] : never ) : - p['items'] extends NonNullable ? SchemaType[] : + p['items'] extends NonNullable ? SchemaTypeDef[] : any[] ) : p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> : diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts deleted file mode 100644 index 7994441791..0000000000 --- a/packages/backend/src/misc/json-value.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type JsonValue = JsonArray | JsonObject | string | number | boolean | null; -export type JsonObject = {[K in string]?: JsonValue}; -export type JsonArray = JsonValue[]; diff --git a/packages/backend/src/misc/langmap.ts b/packages/backend/src/misc/langmap.ts index 5ff9338651..e0d17cbfa5 100644 --- a/packages/backend/src/misc/langmap.ts +++ b/packages/backend/src/misc/langmap.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/loader.ts b/packages/backend/src/misc/loader.ts index 7f29b9db10..25f7b54d31 100644 --- a/packages/backend/src/misc/loader.ts +++ b/packages/backend/src/misc/loader.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export type FetchFunction = (key: K) => Promise; type ResolveReject = Parameters>[0]>; diff --git a/packages/backend/src/misc/normalize-for-search.ts b/packages/backend/src/misc/normalize-for-search.ts index 3f19617e14..2d2155b708 100644 --- a/packages/backend/src/misc/normalize-for-search.ts +++ b/packages/backend/src/misc/normalize-for-search.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts deleted file mode 100644 index a1a3327267..0000000000 --- a/packages/backend/src/misc/password.ts +++ /dev/null @@ -1,20 +0,0 @@ -import bcrypt from 'bcryptjs'; -import * as argon2 from 'argon2'; -import { randomBytes } from 'crypto'; - -export async function hashPassword(password: string): Promise { - const salt = randomBytes(32); - return argon2.hash(password, { salt: salt, type: argon2.argon2id }); -} - -export async function comparePassword(password: string, hash: string): Promise { - if (isOldAlgorithm(hash)) { - return bcrypt.compare(password, hash); - } - - return argon2.verify(hash, password); -} - -export function isOldAlgorithm(hash: string): boolean { - return hash.startsWith('$2'); -} diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts index f741a0c913..8b909d8ec6 100644 --- a/packages/backend/src/misc/prelude/array.ts +++ b/packages/backend/src/misc/prelude/array.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -65,6 +65,44 @@ export function maximum(xs: number[]): number { return Math.max(...xs); } +/** + * Splits an array based on the equivalence relation. + * The concatenation of the result is equal to the argument. + */ +export function groupBy(f: EndoRelation, xs: T[]): T[][] { + const groups = [] as T[][]; + for (const x of xs) { + const lastGroup = groups.at(-1); + if (lastGroup !== undefined && f(lastGroup[0], x)) { + lastGroup.push(x); + } else { + groups.push([x]); + } + } + return groups; +} + +/** + * Splits an array based on the equivalence relation induced by the function. + * The concatenation of the result is equal to the argument. + */ +export function groupOn(f: (x: T) => S, xs: T[]): T[][] { + return groupBy((a, b) => f(a) === f(b), xs); +} + +export function groupByX(collections: T[], keySelector: (x: T) => string) { + return collections.reduce((obj: Record, item: T) => { + const key = keySelector(item); + if (!Object.prototype.hasOwnProperty.call(obj, key)) { + obj[key] = []; + } + + obj[key].push(item); + + return obj; + }, {}); +} + /** * Compare two arrays by lexicographical order */ diff --git a/packages/backend/src/misc/prelude/await-all.ts b/packages/backend/src/misc/prelude/await-all.ts index 48249fe1ae..c653695589 100644 --- a/packages/backend/src/misc/prelude/await-all.ts +++ b/packages/backend/src/misc/prelude/await-all.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/prelude/math.ts b/packages/backend/src/misc/prelude/math.ts new file mode 100644 index 0000000000..83a80b4266 --- /dev/null +++ b/packages/backend/src/misc/prelude/math.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function gcd(a: number, b: number): number { + return b === 0 ? a : gcd(b, a % b); +} diff --git a/packages/backend/src/misc/prelude/maybe.ts b/packages/backend/src/misc/prelude/maybe.ts new file mode 100644 index 0000000000..049686390c --- /dev/null +++ b/packages/backend/src/misc/prelude/maybe.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export interface IMaybe { + isJust(): this is IJust; +} + +export interface IJust extends IMaybe { + get(): T; +} + +export function just(value: T): IJust { + return { + isJust: () => true, + get: () => value, + }; +} + +export function nothing(): IMaybe { + return { + isJust: () => false, + }; +} diff --git a/packages/backend/src/misc/prelude/relation.ts b/packages/backend/src/misc/prelude/relation.ts index 7dcd4c700a..a21f70598b 100644 --- a/packages/backend/src/misc/prelude/relation.ts +++ b/packages/backend/src/misc/prelude/relation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/prelude/string.ts b/packages/backend/src/misc/prelude/string.ts new file mode 100644 index 0000000000..4fe4caea86 --- /dev/null +++ b/packages/backend/src/misc/prelude/string.ts @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function concat(xs: string[]): string { + return xs.join(''); +} + +export function capitalize(s: string): string { + return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1)); +} + +export function toUpperCase(s: string): string { + return s.toUpperCase(); +} + +export function toLowerCase(s: string): string { + return s.toLowerCase(); +} diff --git a/packages/backend/src/misc/prelude/symbol.ts b/packages/backend/src/misc/prelude/symbol.ts new file mode 100644 index 0000000000..88fd285c13 --- /dev/null +++ b/packages/backend/src/misc/prelude/symbol.ts @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const fallback = Symbol('fallback'); diff --git a/packages/backend/src/misc/prelude/time.ts b/packages/backend/src/misc/prelude/time.ts index 275b67ed00..5c66cf2706 100644 --- a/packages/backend/src/misc/prelude/time.ts +++ b/packages/backend/src/misc/prelude/time.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/prelude/url.ts b/packages/backend/src/misc/prelude/url.ts index 270a075075..ae792272f4 100644 --- a/packages/backend/src/misc/prelude/url.ts +++ b/packages/backend/src/misc/prelude/url.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/prelude/xml.ts b/packages/backend/src/misc/prelude/xml.ts index 61c166cee5..755c8f7f0a 100644 --- a/packages/backend/src/misc/prelude/xml.ts +++ b/packages/backend/src/misc/prelude/xml.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/promise-tracker.ts b/packages/backend/src/misc/promise-tracker.ts deleted file mode 100644 index 8a52ca703e..0000000000 --- a/packages/backend/src/misc/promise-tracker.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const promiseRefs: Set>> = new Set(); - -/** - * This tracks promises that other modules decided not to wait for, - * and makes sure they are all settled before fully closing down the server. - */ -export function trackPromise(promise: Promise) { - if (process.env.NODE_ENV !== 'test') { - return; - } - const ref = new WeakRef(promise); - promiseRefs.add(ref); - promise.finally(() => promiseRefs.delete(ref)); -} - -export async function allSettled(): Promise { - await Promise.allSettled([...promiseRefs].map(r => r.deref())); -} diff --git a/packages/backend/src/misc/reset-db.ts b/packages/backend/src/misc/reset-db.ts index 75fb4c3e7b..6acda72c6e 100644 --- a/packages/backend/src/misc/reset-db.ts +++ b/packages/backend/src/misc/reset-db.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts index ac4b8e2e2e..40f9d9e69f 100644 --- a/packages/backend/src/misc/safe-for-sql.ts +++ b/packages/backend/src/misc/safe-for-sql.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts index 7853100d89..a65d870095 100644 --- a/packages/backend/src/misc/secure-rndstr.ts +++ b/packages/backend/src/misc/secure-rndstr.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts index 8ddec35f23..7bc330b7a0 100644 --- a/packages/backend/src/misc/show-machine-info.ts +++ b/packages/backend/src/misc/show-machine-info.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts index ffe61670ee..af176cc939 100644 --- a/packages/backend/src/misc/sql-like-escape.ts +++ b/packages/backend/src/misc/sql-like-escape.ts @@ -1,8 +1,8 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ export function sqlLikeEscape(s: string) { - return s.replace(/([%_\\])/g, '\\$1'); + return s.replace(/([%_])/g, '\\$1'); } diff --git a/packages/backend/src/misc/status-error.ts b/packages/backend/src/misc/status-error.ts index c3533db607..97d1cb6fb0 100644 --- a/packages/backend/src/misc/status-error.ts +++ b/packages/backend/src/misc/status-error.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,6 @@ export class StatusError extends Error { public statusCode: number; public statusMessage?: string; public isClientError: boolean; - public isRetryable: boolean; constructor(message: string, statusCode: number, statusMessage?: string) { super(message); @@ -15,6 +14,5 @@ export class StatusError extends Error { this.statusCode = statusCode; this.statusMessage = statusMessage; this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500; - this.isRetryable = !this.isClientError || this.statusCode === 429; } } diff --git a/packages/backend/src/misc/truncate.ts b/packages/backend/src/misc/truncate.ts index 1c8a274609..e7e1442eaf 100644 --- a/packages/backend/src/misc/truncate.ts +++ b/packages/backend/src/misc/truncate.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts deleted file mode 100644 index fbff880afc..0000000000 --- a/packages/backend/src/models/AbuseReportNotificationRecipient.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { MiSystemWebhook } from '@/models/SystemWebhook.js'; -import { MiUserProfile } from '@/models/UserProfile.js'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -/** - * 通報受信時に通知を送信する方法. - */ -export type RecipientMethod = 'email' | 'webhook'; - -@Entity('abuse_report_notification_recipient') -export class MiAbuseReportNotificationRecipient { - @PrimaryColumn(id()) - public id: string; - - /** - * 有効かどうか. - */ - @Index() - @Column('boolean', { - default: true, - }) - public isActive: boolean; - - /** - * 更新日時. - */ - @Column('timestamp with time zone', { - default: () => 'CURRENT_TIMESTAMP', - }) - public updatedAt: Date; - - /** - * 通知設定名. - */ - @Column('varchar', { - length: 255, - }) - public name: string; - - /** - * 通知方法. - */ - @Index() - @Column('varchar', { - length: 64, - }) - public method: RecipientMethod; - - /** - * 通知先のユーザID. - */ - @Index() - @Column({ - ...id(), - nullable: true, - }) - public userId: MiUser['id'] | null; - - /** - * 通知先のユーザ. - */ - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' }) - public user: MiUser | null; - - /** - * 通知先のユーザプロフィール. - */ - @ManyToOne(type => MiUserProfile, {}) - @JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' }) - public userProfile: MiUserProfile | null; - - /** - * 通知先のシステムWebhookId. - */ - @Index() - @Column({ - ...id(), - nullable: true, - }) - public systemWebhookId: string | null; - - /** - * 通知先のシステムWebhook. - */ - @ManyToOne(type => MiSystemWebhook, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public systemWebhook: MiSystemWebhook | null; -} diff --git a/packages/backend/src/models/AbuseReportResolver.ts b/packages/backend/src/models/AbuseReportResolver.ts index f4c4224481..bdcaeeb729 100644 --- a/packages/backend/src/models/AbuseReportResolver.ts +++ b/packages/backend/src/models/AbuseReportResolver.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts index 0615fd7eb5..955d0a5e56 100644 --- a/packages/backend/src/models/AbuseUserReport.ts +++ b/packages/backend/src/models/AbuseUserReport.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/AccessToken.ts b/packages/backend/src/models/AccessToken.ts index 6f98c14ec1..dd9efc8ffd 100644 --- a/packages/backend/src/models/AccessToken.ts +++ b/packages/backend/src/models/AccessToken.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Ad.ts b/packages/backend/src/models/Ad.ts index 108e991c70..0de12befc8 100644 --- a/packages/backend/src/models/Ad.ts +++ b/packages/backend/src/models/Ad.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Announcement.ts b/packages/backend/src/models/Announcement.ts index d0c59fff50..ddca1e4b69 100644 --- a/packages/backend/src/models/Announcement.ts +++ b/packages/backend/src/models/Announcement.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -38,7 +38,7 @@ export class MiAnnouncement { length: 256, nullable: false, default: 'info', }) - public icon: 'info' | 'warning' | 'error' | 'success'; + public icon: string; // normal ... お知らせページ掲載 // banner ... お知らせページ掲載 + バナー表示 @@ -47,7 +47,7 @@ export class MiAnnouncement { length: 256, nullable: false, default: 'normal', }) - public display: 'normal' | 'banner' | 'dialog'; + public display: string; @Column('boolean', { default: false, diff --git a/packages/backend/src/models/AnnouncementRead.ts b/packages/backend/src/models/AnnouncementRead.ts index 47de8dd180..1fb1b965bd 100644 --- a/packages/backend/src/models/AnnouncementRead.ts +++ b/packages/backend/src/models/AnnouncementRead.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts index 98b43a3347..a2c54322a1 100644 --- a/packages/backend/src/models/Antenna.ts +++ b/packages/backend/src/models/Antenna.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -85,11 +85,6 @@ export class MiAntenna { }) public caseSensitive: boolean; - @Column('boolean', { - default: false, - }) - public excludeBots: boolean; - @Column('boolean', { default: false, }) @@ -103,6 +98,9 @@ export class MiAntenna { }) public expression: string | null; + @Column('boolean') + public notify: boolean; + @Index() @Column('boolean', { default: true, diff --git a/packages/backend/src/models/App.ts b/packages/backend/src/models/App.ts index 0185e2995c..521f5f50e2 100644 --- a/packages/backend/src/models/App.ts +++ b/packages/backend/src/models/App.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/AuthSession.ts b/packages/backend/src/models/AuthSession.ts index 03050ba955..f1cb0b52ed 100644 --- a/packages/backend/src/models/AuthSession.ts +++ b/packages/backend/src/models/AuthSession.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts index ccfcb91f5a..7be5b47cd8 100644 --- a/packages/backend/src/models/AvatarDecoration.ts +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Blocking.ts b/packages/backend/src/models/Blocking.ts index 34a6efe5a6..571b7d8b99 100644 --- a/packages/backend/src/models/Blocking.ts +++ b/packages/backend/src/models/Blocking.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/BubbleGameRecord.ts b/packages/backend/src/models/BubbleGameRecord.ts deleted file mode 100644 index 686e39c118..0000000000 --- a/packages/backend/src/models/BubbleGameRecord.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('bubble_game_record') -export class MiBubbleGameRecord { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column('timestamp with time zone') - public seededAt: Date; - - @Column('varchar', { - length: 1024, - }) - public seed: string; - - @Column('integer') - public gameVersion: number; - - @Column('varchar', { - length: 128, - }) - public gameMode: string; - - @Index() - @Column('integer') - public score: number; - - @Column('jsonb', { - default: [], - }) - public logs: number[][]; - - @Column('boolean', { - default: false, - }) - public isVerified: boolean; -} diff --git a/packages/backend/src/models/Channel.ts b/packages/backend/src/models/Channel.ts index f5e9b17e3e..ea9e993c9a 100644 --- a/packages/backend/src/models/Channel.ts +++ b/packages/backend/src/models/Channel.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/ChannelFavorite.ts b/packages/backend/src/models/ChannelFavorite.ts index 167f41cf16..272411cac3 100644 --- a/packages/backend/src/models/ChannelFavorite.ts +++ b/packages/backend/src/models/ChannelFavorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/ChannelFollowing.ts b/packages/backend/src/models/ChannelFollowing.ts index c7afdd05b0..ad245f14b5 100644 --- a/packages/backend/src/models/ChannelFollowing.ts +++ b/packages/backend/src/models/ChannelFollowing.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Clip.ts b/packages/backend/src/models/Clip.ts index 6295a329fb..54f3c97c63 100644 --- a/packages/backend/src/models/Clip.ts +++ b/packages/backend/src/models/Clip.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/ClipFavorite.ts b/packages/backend/src/models/ClipFavorite.ts index 40bdb9f4aa..4a00d60e97 100644 --- a/packages/backend/src/models/ClipFavorite.ts +++ b/packages/backend/src/models/ClipFavorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/ClipNote.ts b/packages/backend/src/models/ClipNote.ts index 6e1d2bec4c..d453c09bcb 100644 --- a/packages/backend/src/models/ClipNote.ts +++ b/packages/backend/src/models/ClipNote.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts index 7b03e3e494..ba4226a592 100644 --- a/packages/backend/src/models/DriveFile.ts +++ b/packages/backend/src/models/DriveFile.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -82,7 +82,7 @@ export class MiDriveFile { public storedInternal: boolean; @Column('varchar', { - length: 1024, + length: 512, comment: 'The URL of the DriveFile.', }) public url: string; @@ -124,13 +124,13 @@ export class MiDriveFile { @Index() @Column('varchar', { - length: 1024, nullable: true, + length: 512, nullable: true, comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.', }) public uri: string | null; @Column('varchar', { - length: 1024, nullable: true, + length: 512, nullable: true, }) public src: string | null; diff --git a/packages/backend/src/models/DriveFolder.ts b/packages/backend/src/models/DriveFolder.ts index 07046d6e11..7a71ac001e 100644 --- a/packages/backend/src/models/DriveFolder.ts +++ b/packages/backend/src/models/DriveFolder.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/Emoji.ts index d62b6e9f6f..64c58b43e5 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Event.ts b/packages/backend/src/models/Event.ts index 39e5552225..49293dd232 100644 --- a/packages/backend/src/models/Event.ts +++ b/packages/backend/src/models/Event.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts index a1469a0d94..8df28a62c1 100644 --- a/packages/backend/src/models/Flash.ts +++ b/packages/backend/src/models/Flash.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/FlashLike.ts b/packages/backend/src/models/FlashLike.ts index a9fb48123e..5362a7fe63 100644 --- a/packages/backend/src/models/FlashLike.ts +++ b/packages/backend/src/models/FlashLike.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/FollowRequest.ts b/packages/backend/src/models/FollowRequest.ts index 3ff5e7a478..e90e03464c 100644 --- a/packages/backend/src/models/FollowRequest.ts +++ b/packages/backend/src/models/FollowRequest.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts index 62cbc29f26..a9658ee13c 100644 --- a/packages/backend/src/models/Following.ts +++ b/packages/backend/src/models/Following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/GalleryLike.ts b/packages/backend/src/models/GalleryLike.ts index ed0963122d..a6288b87f6 100644 --- a/packages/backend/src/models/GalleryLike.ts +++ b/packages/backend/src/models/GalleryLike.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/GalleryPost.ts b/packages/backend/src/models/GalleryPost.ts index 04d8823e37..3b54a221d8 100644 --- a/packages/backend/src/models/GalleryPost.ts +++ b/packages/backend/src/models/GalleryPost.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Hashtag.ts b/packages/backend/src/models/Hashtag.ts index 3add06d0c3..18a49ad6e1 100644 --- a/packages/backend/src/models/Hashtag.ts +++ b/packages/backend/src/models/Hashtag.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts index 17cd5c6665..417d677128 100644 --- a/packages/backend/src/models/Instance.ts +++ b/packages/backend/src/models/Instance.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -81,22 +81,13 @@ export class MiInstance { public isNotResponding: boolean; /** - * このインスタンスと不通になった日時 - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public notRespondingSince: Date | null; - - /** - * このインスタンスへの配信状態 + * このインスタンスへの配信を停止するか */ @Index() - @Column('enum', { - default: 'none', - enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'], + @Column('boolean', { + default: false, }) - public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'; + public isSuspended: boolean; @Column('varchar', { length: 64, nullable: true, @@ -153,9 +144,4 @@ export class MiInstance { nullable: true, }) public infoUpdatedAt: Date | null; - - @Column('varchar', { - length: 16384, default: '', - }) - public moderationNote: string; } diff --git a/packages/backend/src/models/MessagingMessage.ts b/packages/backend/src/models/MessagingMessage.ts index 1357438679..80b3c2c4c2 100644 --- a/packages/backend/src/models/MessagingMessage.ts +++ b/packages/backend/src/models/MessagingMessage.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 803c80513e..3de279800c 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -76,21 +76,11 @@ export class MiMeta { }) public sensitiveWords: string[]; - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public prohibitedWords: string[]; - @Column('varchar', { length: 1024, array: true, default: '{}', }) public silencedHosts: string[]; - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public mediaSilencedHosts: string[]; - @Column('varchar', { length: 1024, nullable: true, @@ -201,29 +191,6 @@ export class MiMeta { }) public hcaptchaSecretKey: string | null; - @Column('boolean', { - default: false, - }) - public enableMcaptcha: boolean; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public mcaptchaSitekey: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public mcaptchaSecretKey: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public mcaptchaInstanceUrl: string | null; - @Column('boolean', { default: false, }) @@ -258,8 +225,6 @@ export class MiMeta { }) public turnstileSecretKey: string | null; - // chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること - @Column('enum', { enum: ['none', 'all', 'local', 'remote'], default: 'none', @@ -282,10 +247,11 @@ export class MiMeta { }) public enableSensitiveMediaDetectionForVideos: boolean; - @Column('boolean', { - default: false, + @Column('varchar', { + length: 1024, + nullable: true, }) - public directSummalyProxy: boolean; + public summalyProxy: string | null; @Column('boolean', { default: false, @@ -399,9 +365,9 @@ export class MiMeta { @Column('varchar', { length: 1024, default: 'https://github.com/kokonect-link/cherrypick', - nullable: true, + nullable: false, }) - public repositoryUrl: string | null; + public repositoryUrl: string; @Column('varchar', { length: 1024, @@ -422,13 +388,6 @@ export class MiMeta { }) public privacyPolicyUrl: string | null; - @Column('varchar', { - length: 1024, - nullable: true, - }) - public statusUrl: string | null; - public inquiryUrl: string | null; - @Column('varchar', { length: 8192, nullable: true, @@ -606,23 +565,6 @@ export class MiMeta { }) public verifymailAuthKey: string | null; - @Column('boolean', { - default: false, - }) - public enableTruemailApi: boolean; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public truemailInstance: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public truemailAuthKey: string | null; - @Column('boolean', { default: true, }) @@ -708,38 +650,6 @@ export class MiMeta { }) public notesPerOneAd: number; - @Column('boolean', { - default: true, - }) - public urlPreviewEnabled: boolean; - - @Column('integer', { - default: 10000, - }) - public urlPreviewTimeout: number; - - @Column('bigint', { - default: 1024 * 1024 * 10, - }) - public urlPreviewMaximumContentLength: number; - - @Column('boolean', { - default: true, - }) - public urlPreviewRequireContentLength: boolean; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public urlPreviewSummaryProxyUrl: string | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public urlPreviewUserAgent: string | null; - @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/ModerationLog.ts b/packages/backend/src/models/ModerationLog.ts index edde315fdf..dc43a192fa 100644 --- a/packages/backend/src/models/ModerationLog.ts +++ b/packages/backend/src/models/ModerationLog.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Muting.ts b/packages/backend/src/models/Muting.ts index e1240b9c4e..d4ce4e0788 100644 --- a/packages/backend/src/models/Muting.ts +++ b/packages/backend/src/models/Muting.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index 39079caaf5..2573334dec 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -11,6 +11,9 @@ import { MiChannel } from './Channel.js'; import type { MiDriveFile } from './DriveFile.js'; @Entity('note') +@Index('IDX_NOTE_TAGS', { synchronize: false }) +@Index('IDX_NOTE_MENTIONS', { synchronize: false }) +@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) export class MiNote { @PrimaryColumn(id()) public id: string; @@ -42,8 +45,7 @@ export class MiNote { public replyId: MiNote['id'] | null; @ManyToOne(type => MiNote, { - // onDelete: 'CASCADE', - onDelete: 'SET NULL', + onDelete: 'CASCADE', }) @JoinColumn() public reply: MiNote | null; @@ -142,7 +144,6 @@ export class MiNote { * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す * followers ... フォロワーのみ * specified ... visibleUserIds で指定したユーザーのみ - * private ... 投稿者のみ */ @Column('enum', { enum: noteVisibilities }) public visibility: typeof noteVisibilities[number]; @@ -160,7 +161,7 @@ export class MiNote { }) public url: string | null; - @Index('IDX_NOTE_FILE_IDS', { synchronize: false }) + @Index() @Column({ ...id(), array: true, default: '{}', @@ -172,14 +173,14 @@ export class MiNote { }) public attachedFileTypes: string[]; - @Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) + @Index() @Column({ ...id(), array: true, default: '{}', }) public visibleUserIds: MiUser['id'][]; - @Index('IDX_NOTE_MENTIONS', { synchronize: false }) + @Index() @Column({ ...id(), array: true, default: '{}', @@ -201,7 +202,7 @@ export class MiNote { }) public emojis: string[]; - @Index('IDX_NOTE_TAGS', { synchronize: false }) + @Index() @Column('varchar', { length: 128, array: true, default: '{}', }) @@ -212,11 +213,6 @@ export class MiNote { }) public hasPoll: boolean; - @Column('timestamp with time zone', { - nullable: true, - }) - public deleteAt: Date | null; - @Index() @Column({ ...id(), diff --git a/packages/backend/src/models/NoteFavorite.ts b/packages/backend/src/models/NoteFavorite.ts index cf76c767b0..30a055047e 100644 --- a/packages/backend/src/models/NoteFavorite.ts +++ b/packages/backend/src/models/NoteFavorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/NoteReaction.ts b/packages/backend/src/models/NoteReaction.ts index 42dfcaa9ad..3b378a1285 100644 --- a/packages/backend/src/models/NoteReaction.ts +++ b/packages/backend/src/models/NoteReaction.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/NoteThreadMuting.ts b/packages/backend/src/models/NoteThreadMuting.ts index e7bd39f348..43058762e8 100644 --- a/packages/backend/src/models/NoteThreadMuting.ts +++ b/packages/backend/src/models/NoteThreadMuting.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/NoteUnread.ts b/packages/backend/src/models/NoteUnread.ts index c759181117..e020ef8667 100644 --- a/packages/backend/src/models/NoteUnread.ts +++ b/packages/backend/src/models/NoteUnread.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 68ece6b947..81d000bb35 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -69,16 +69,16 @@ export type MiNotification = { createdAt: string; notifierId: MiUser['id']; } | { - type: 'groupInvited'; + type: 'roleAssigned'; id: string; createdAt: string; - notifierId: MiUser['id']; - userGroupInvitationId: MiUserGroupInvitation['id']; + roleId: MiRole['id']; } | { - type: 'roleAssigned'; + type: 'groupInvited'; id: string; createdAt: string; - roleId: MiRole['id']; + notifierId: MiUser['id']; + userGroupInvitationId: MiUserGroupInvitation['id']; } | { type: 'achievementEarned'; id: string; diff --git a/packages/backend/src/models/Page.ts b/packages/backend/src/models/Page.ts index 1695bf570e..05b6ccd714 100644 --- a/packages/backend/src/models/Page.ts +++ b/packages/backend/src/models/Page.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/PageLike.ts b/packages/backend/src/models/PageLike.ts index 05ca22cf2c..46b57f8304 100644 --- a/packages/backend/src/models/PageLike.ts +++ b/packages/backend/src/models/PageLike.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/PasswordResetRequest.ts b/packages/backend/src/models/PasswordResetRequest.ts index fdaf21056b..a7420f35b9 100644 --- a/packages/backend/src/models/PasswordResetRequest.ts +++ b/packages/backend/src/models/PasswordResetRequest.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Poll.ts b/packages/backend/src/models/Poll.ts index 89e576489c..15131e9fdc 100644 --- a/packages/backend/src/models/Poll.ts +++ b/packages/backend/src/models/Poll.ts @@ -1,11 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; import { noteVisibilities } from '@/types.js'; -import type { MiChannel } from '@/models/Channel.js'; import { id } from './util/id.js'; import { MiNote } from './Note.js'; import type { MiUser } from './User.js'; @@ -59,14 +58,6 @@ export class MiPoll { comment: '[Denormalized]', }) public userHost: string | null; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: '[Denormalized]', - }) - public channelId: MiChannel['id'] | null; //#endregion constructor(data: Partial) { diff --git a/packages/backend/src/models/PollVote.ts b/packages/backend/src/models/PollVote.ts index b5c780293c..89b6ed6e9e 100644 --- a/packages/backend/src/models/PollVote.ts +++ b/packages/backend/src/models/PollVote.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/PromoNote.ts b/packages/backend/src/models/PromoNote.ts index ae27adec9e..052da8ea5a 100644 --- a/packages/backend/src/models/PromoNote.ts +++ b/packages/backend/src/models/PromoNote.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/PromoRead.ts b/packages/backend/src/models/PromoRead.ts index b2a698cc7b..f2b43eb885 100644 --- a/packages/backend/src/models/PromoRead.ts +++ b/packages/backend/src/models/PromoRead.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/RegistrationTicket.ts b/packages/backend/src/models/RegistrationTicket.ts index 0a4e4b9189..f8986c9ed3 100644 --- a/packages/backend/src/models/RegistrationTicket.ts +++ b/packages/backend/src/models/RegistrationTicket.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/RegistryItem.ts b/packages/backend/src/models/RegistryItem.ts index 335e8b9eab..70a9e12dde 100644 --- a/packages/backend/src/models/RegistryItem.ts +++ b/packages/backend/src/models/RegistryItem.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Relay.ts b/packages/backend/src/models/Relay.ts index eca2916032..1a261802c8 100644 --- a/packages/backend/src/models/Relay.ts +++ b/packages/backend/src/models/Relay.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/RenoteMuting.ts b/packages/backend/src/models/RenoteMuting.ts index 448a0b7663..ad1459b3a7 100644 --- a/packages/backend/src/models/RenoteMuting.ts +++ b/packages/backend/src/models/RenoteMuting.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index facd56ba08..94b19959dd 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,537 +1,443 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { - MiAbuseReportNotificationRecipient, - MiAbuseReportResolver, - MiAbuseUserReport, - MiAccessToken, - MiAd, - MiAnnouncement, - MiAnnouncementRead, - MiAntenna, - MiApp, - MiAuthSession, - MiAvatarDecoration, - MiBlocking, - MiBubbleGameRecord, - MiChannel, - MiChannelFavorite, - MiChannelFollowing, - MiClip, - MiClipFavorite, - MiClipNote, - MiDriveFile, - MiDriveFolder, - MiEmoji, - MiEvent, - MiFlash, - MiFlashLike, - MiFollowing, - MiFollowRequest, - MiGalleryLike, - MiGalleryPost, - MiHashtag, - MiInstance, - MiMessagingMessage, - MiMeta, - MiModerationLog, - MiMuting, - MiNote, - MiNoteFavorite, - MiNoteReaction, - MiNoteThreadMuting, - MiNoteUnread, - MiPage, - MiPageLike, - MiPasswordResetRequest, - MiPoll, - MiPollVote, - MiPromoNote, - MiPromoRead, - MiRegistrationTicket, - MiRegistryItem, - MiRelay, - MiRenoteMuting, - MiRepository, - miRepository, - MiRetentionAggregation, - MiRole, - MiRoleAssignment, - MiSignin, - MiSwSubscription, - MiSystemWebhook, - MiUsedUsername, - MiUser, - MiUserGroup, - MiUserGroupJoining, - MiUserGroupInvitation, - MiUserIp, - MiUserKeypair, - MiUserList, - MiUserListFavorite, - MiUserListMembership, - MiUserMemo, - MiUserNotePining, - MiUserPending, - MiUserProfile, - MiUserPublickey, - MiUserSecurityKey, - MiWebhook, -} from './_.js'; -import type { Provider } from '@nestjs/common'; +import { MiAbuseReportResolver, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiEvent, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMessagingMessage, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserGroup, MiUserGroupJoining, MiUserGroupInvitation, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js'; import type { DataSource } from 'typeorm'; +import type { Provider } from '@nestjs/common'; const $usersRepository: Provider = { provide: DI.usersRepository, - useFactory: (db: DataSource) => db.getRepository(MiUser).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUser), inject: [DI.db], }; const $notesRepository: Provider = { provide: DI.notesRepository, - useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiNote), inject: [DI.db], }; const $announcementsRepository: Provider = { provide: DI.announcementsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiAnnouncement), inject: [DI.db], }; const $announcementReadsRepository: Provider = { provide: DI.announcementReadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead), inject: [DI.db], }; const $appsRepository: Provider = { provide: DI.appsRepository, - useFactory: (db: DataSource) => db.getRepository(MiApp).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiApp), inject: [DI.db], }; const $avatarDecorationsRepository: Provider = { provide: DI.avatarDecorationsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration), inject: [DI.db], }; const $noteFavoritesRepository: Provider = { provide: DI.noteFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite), inject: [DI.db], }; const $noteThreadMutingsRepository: Provider = { provide: DI.noteThreadMutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting), inject: [DI.db], }; const $noteReactionsRepository: Provider = { provide: DI.noteReactionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteReaction).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiNoteReaction), inject: [DI.db], }; const $noteUnreadsRepository: Provider = { provide: DI.noteUnreadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiNoteUnread), inject: [DI.db], }; const $pollsRepository: Provider = { provide: DI.pollsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiPoll), inject: [DI.db], }; const $pollVotesRepository: Provider = { provide: DI.pollVotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPollVote).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiPollVote), inject: [DI.db], }; const $userProfilesRepository: Provider = { provide: DI.userProfilesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserProfile).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserProfile), inject: [DI.db], }; const $userKeypairsRepository: Provider = { provide: DI.userKeypairsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserKeypair).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserKeypair), inject: [DI.db], }; const $userPendingsRepository: Provider = { provide: DI.userPendingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserPending).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserPending), inject: [DI.db], }; const $userSecurityKeysRepository: Provider = { provide: DI.userSecurityKeysRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey), inject: [DI.db], }; const $userPublickeysRepository: Provider = { provide: DI.userPublickeysRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserPublickey).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserPublickey), inject: [DI.db], }; const $userListsRepository: Provider = { provide: DI.userListsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserList), inject: [DI.db], }; const $userListFavoritesRepository: Provider = { provide: DI.userListFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite), inject: [DI.db], }; const $userListMembershipsRepository: Provider = { provide: DI.userListMembershipsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserListMembership).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserListMembership), inject: [DI.db], }; const $userGroupsRepository: Provider = { provide: DI.userGroupsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserGroup).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserGroup), inject: [DI.db], }; const $userGroupJoiningsRepository: Provider = { provide: DI.userGroupJoiningsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserGroupJoining).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserGroupJoining), inject: [DI.db], }; const $userGroupInvitationsRepository: Provider = { provide: DI.userGroupInvitationsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserGroupInvitation).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserGroupInvitation), inject: [DI.db], }; const $userNotePiningsRepository: Provider = { provide: DI.userNotePiningsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserNotePining), inject: [DI.db], }; const $userIpsRepository: Provider = { provide: DI.userIpsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserIp), inject: [DI.db], }; const $usedUsernamesRepository: Provider = { provide: DI.usedUsernamesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUsedUsername).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUsedUsername), inject: [DI.db], }; const $followingsRepository: Provider = { provide: DI.followingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFollowing).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiFollowing), inject: [DI.db], }; const $followRequestsRepository: Provider = { provide: DI.followRequestsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFollowRequest).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiFollowRequest), inject: [DI.db], }; const $instancesRepository: Provider = { provide: DI.instancesRepository, - useFactory: (db: DataSource) => db.getRepository(MiInstance).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiInstance), inject: [DI.db], }; const $emojisRepository: Provider = { provide: DI.emojisRepository, - useFactory: (db: DataSource) => db.getRepository(MiEmoji).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiEmoji), inject: [DI.db], }; const $eventsRepository: Provider = { provide: DI.eventsRepository, - useFactory: (db: DataSource) => db.getRepository(MiEvent).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiEvent), inject: [DI.db], }; const $driveFilesRepository: Provider = { provide: DI.driveFilesRepository, - useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiDriveFile), inject: [DI.db], }; const $driveFoldersRepository: Provider = { provide: DI.driveFoldersRepository, - useFactory: (db: DataSource) => db.getRepository(MiDriveFolder).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiDriveFolder), inject: [DI.db], }; const $metasRepository: Provider = { provide: DI.metasRepository, - useFactory: (db: DataSource) => db.getRepository(MiMeta).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiMeta), inject: [DI.db], }; const $mutingsRepository: Provider = { provide: DI.mutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiMuting).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiMuting), inject: [DI.db], }; const $renoteMutingsRepository: Provider = { provide: DI.renoteMutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting), inject: [DI.db], }; const $blockingsRepository: Provider = { provide: DI.blockingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiBlocking).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiBlocking), inject: [DI.db], }; const $swSubscriptionsRepository: Provider = { provide: DI.swSubscriptionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiSwSubscription), inject: [DI.db], }; const $hashtagsRepository: Provider = { provide: DI.hashtagsRepository, - useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiHashtag), inject: [DI.db], }; const $abuseUserReportsRepository: Provider = { provide: DI.abuseUserReportsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $abuseReportNotificationRecipientRepository: Provider = { - provide: DI.abuseReportNotificationRecipientRepository, - useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient), + useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport), inject: [DI.db], }; const $registrationTicketsRepository: Provider = { provide: DI.registrationTicketsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket), inject: [DI.db], }; const $authSessionsRepository: Provider = { provide: DI.authSessionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAuthSession).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiAuthSession), inject: [DI.db], }; const $accessTokensRepository: Provider = { provide: DI.accessTokensRepository, - useFactory: (db: DataSource) => db.getRepository(MiAccessToken).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiAccessToken), inject: [DI.db], }; const $signinsRepository: Provider = { provide: DI.signinsRepository, - useFactory: (db: DataSource) => db.getRepository(MiSignin).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiSignin), inject: [DI.db], }; const $messagingMessagesRepository: Provider = { provide: DI.messagingMessagesRepository, - useFactory: (db: DataSource) => db.getRepository(MiMessagingMessage).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiMessagingMessage), inject: [DI.db], }; const $pagesRepository: Provider = { provide: DI.pagesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPage).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiPage), inject: [DI.db], }; const $pageLikesRepository: Provider = { provide: DI.pageLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPageLike).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiPageLike), inject: [DI.db], }; const $galleryPostsRepository: Provider = { provide: DI.galleryPostsRepository, - useFactory: (db: DataSource) => db.getRepository(MiGalleryPost).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiGalleryPost), inject: [DI.db], }; const $galleryLikesRepository: Provider = { provide: DI.galleryLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiGalleryLike).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiGalleryLike), inject: [DI.db], }; const $moderationLogsRepository: Provider = { provide: DI.moderationLogsRepository, - useFactory: (db: DataSource) => db.getRepository(MiModerationLog).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiModerationLog), inject: [DI.db], }; const $clipsRepository: Provider = { provide: DI.clipsRepository, - useFactory: (db: DataSource) => db.getRepository(MiClip).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiClip), inject: [DI.db], }; const $clipNotesRepository: Provider = { provide: DI.clipNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiClipNote).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiClipNote), inject: [DI.db], }; const $clipFavoritesRepository: Provider = { provide: DI.clipFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiClipFavorite).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiClipFavorite), inject: [DI.db], }; const $antennasRepository: Provider = { provide: DI.antennasRepository, - useFactory: (db: DataSource) => db.getRepository(MiAntenna).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiAntenna), inject: [DI.db], }; const $promoNotesRepository: Provider = { provide: DI.promoNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiPromoNote), inject: [DI.db], }; const $promoReadsRepository: Provider = { provide: DI.promoReadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPromoRead).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiPromoRead), inject: [DI.db], }; const $relaysRepository: Provider = { provide: DI.relaysRepository, - useFactory: (db: DataSource) => db.getRepository(MiRelay).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiRelay), inject: [DI.db], }; const $channelsRepository: Provider = { provide: DI.channelsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannel).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiChannel), inject: [DI.db], }; const $channelFollowingsRepository: Provider = { provide: DI.channelFollowingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing), inject: [DI.db], }; const $channelFavoritesRepository: Provider = { provide: DI.channelFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite), inject: [DI.db], }; const $registryItemsRepository: Provider = { provide: DI.registryItemsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiRegistryItem), inject: [DI.db], }; const $webhooksRepository: Provider = { provide: DI.webhooksRepository, - useFactory: (db: DataSource) => db.getRepository(MiWebhook).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $systemWebhooksRepository: Provider = { - provide: DI.systemWebhooksRepository, - useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook), + useFactory: (db: DataSource) => db.getRepository(MiWebhook), inject: [DI.db], }; const $adsRepository: Provider = { provide: DI.adsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiAd), inject: [DI.db], }; const $passwordResetRequestsRepository: Provider = { provide: DI.passwordResetRequestsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest), inject: [DI.db], }; const $retentionAggregationsRepository: Provider = { provide: DI.retentionAggregationsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation), inject: [DI.db], }; const $flashsRepository: Provider = { provide: DI.flashsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFlash).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiFlash), inject: [DI.db], }; const $flashLikesRepository: Provider = { provide: DI.flashLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiFlashLike).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiFlashLike), inject: [DI.db], }; const $rolesRepository: Provider = { provide: DI.rolesRepository, - useFactory: (db: DataSource) => db.getRepository(MiRole).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiRole), inject: [DI.db], }; const $roleAssignmentsRepository: Provider = { provide: DI.roleAssignmentsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment), inject: [DI.db], }; const $userMemosRepository: Provider = { provide: DI.userMemosRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserMemo).extend(miRepository as MiRepository), - inject: [DI.db], -}; - -const $bubbleGameRecordsRepository: Provider = { - provide: DI.bubbleGameRecordsRepository, - useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserMemo), inject: [DI.db], }; const $abuseReportResolversRepository: Provider = { provide: DI.abuseReportResolversRepository, - useFactory: (db: DataSource) => db.getRepository(MiAbuseReportResolver).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiAbuseReportResolver), inject: [DI.db], }; @Module({ - imports: [], + imports: [ + ], providers: [ $usersRepository, $notesRepository, @@ -573,7 +479,6 @@ const $abuseReportResolversRepository: Provider = { $swSubscriptionsRepository, $hashtagsRepository, $abuseUserReportsRepository, - $abuseReportNotificationRecipientRepository, $registrationTicketsRepository, $authSessionsRepository, $accessTokensRepository, @@ -596,7 +501,6 @@ const $abuseReportResolversRepository: Provider = { $channelFavoritesRepository, $registryItemsRepository, $webhooksRepository, - $systemWebhooksRepository, $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, @@ -606,7 +510,6 @@ const $abuseReportResolversRepository: Provider = { $flashLikesRepository, $userMemosRepository, $abuseReportResolversRepository, - $bubbleGameRecordsRepository, ], exports: [ $usersRepository, @@ -649,7 +552,6 @@ const $abuseReportResolversRepository: Provider = { $swSubscriptionsRepository, $hashtagsRepository, $abuseUserReportsRepository, - $abuseReportNotificationRecipientRepository, $registrationTicketsRepository, $authSessionsRepository, $accessTokensRepository, @@ -672,7 +574,6 @@ const $abuseReportResolversRepository: Provider = { $channelFavoritesRepository, $registryItemsRepository, $webhooksRepository, - $systemWebhooksRepository, $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, @@ -682,8 +583,6 @@ const $abuseReportResolversRepository: Provider = { $flashLikesRepository, $userMemosRepository, $abuseReportResolversRepository, - $bubbleGameRecordsRepository, ], }) -export class RepositoryModule { -} +export class RepositoryModule {} diff --git a/packages/backend/src/models/RetentionAggregation.ts b/packages/backend/src/models/RetentionAggregation.ts index 139f3e4dfd..794082f376 100644 --- a/packages/backend/src/models/RetentionAggregation.ts +++ b/packages/backend/src/models/RetentionAggregation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts index a173971b2c..07b70a6f94 100644 --- a/packages/backend/src/models/Role.ts +++ b/packages/backend/src/models/Role.ts @@ -1,171 +1,80 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Entity, Column, PrimaryColumn } from 'typeorm'; import { id } from './util/id.js'; -/** - * ~かつ~ - * 複数の条件を同時に満たす場合のみ成立とする - */ type CondFormulaValueAnd = { type: 'and'; values: RoleCondFormulaValue[]; }; -/** - * ~または~ - * 複数の条件のうち、いずれかを満たす場合のみ成立とする - */ type CondFormulaValueOr = { type: 'or'; values: RoleCondFormulaValue[]; }; -/** - * ~ではない - * 条件を満たさない場合のみ成立とする - */ type CondFormulaValueNot = { type: 'not'; value: RoleCondFormulaValue; }; -/** - * ローカルユーザーのみ成立とする - */ type CondFormulaValueIsLocal = { type: 'isLocal'; }; -/** - * リモートユーザーのみ成立とする - */ type CondFormulaValueIsRemote = { type: 'isRemote'; }; -/** - * 既に指定のマニュアルロールにアサインされている場合のみ成立とする - */ -type CondFormulaValueRoleAssignedTo = { - type: 'roleAssignedTo'; - roleId: string; -}; - -/** - * サスペンド済みアカウントの場合のみ成立とする - */ -type CondFormulaValueIsSuspended = { - type: 'isSuspended'; -}; - -/** - * 鍵アカウントの場合のみ成立とする - */ -type CondFormulaValueIsLocked = { - type: 'isLocked'; -}; - -/** - * botアカウントの場合のみ成立とする - */ -type CondFormulaValueIsBot = { - type: 'isBot'; -}; - -/** - * 猫アカウントの場合のみ成立とする - */ -type CondFormulaValueIsCat = { - type: 'isCat'; -}; - -/** - * 「ユーザを見つけやすくする」が有効なアカウントの場合のみ成立とする - */ -type CondFormulaValueIsExplorable = { - type: 'isExplorable'; -}; - -/** - * ユーザが作成されてから指定期間経過した場合のみ成立とする - */ type CondFormulaValueCreatedLessThan = { type: 'createdLessThan'; sec: number; }; -/** - * ユーザが作成されてから指定期間経っていない場合のみ成立とする - */ type CondFormulaValueCreatedMoreThan = { type: 'createdMoreThan'; sec: number; }; -/** - * フォロワー数が指定値以下の場合のみ成立とする - */ type CondFormulaValueFollowersLessThanOrEq = { type: 'followersLessThanOrEq'; value: number; }; -/** - * フォロワー数が指定値以上の場合のみ成立とする - */ type CondFormulaValueFollowersMoreThanOrEq = { type: 'followersMoreThanOrEq'; value: number; }; -/** - * フォロー数が指定値以下の場合のみ成立とする - */ type CondFormulaValueFollowingLessThanOrEq = { type: 'followingLessThanOrEq'; value: number; }; -/** - * フォロー数が指定値以上の場合のみ成立とする - */ type CondFormulaValueFollowingMoreThanOrEq = { type: 'followingMoreThanOrEq'; value: number; }; -/** - * 投稿数が指定値以下の場合のみ成立とする - */ type CondFormulaValueNotesLessThanOrEq = { type: 'notesLessThanOrEq'; value: number; }; -/** - * 投稿数が指定値以上の場合のみ成立とする - */ type CondFormulaValueNotesMoreThanOrEq = { type: 'notesMoreThanOrEq'; value: number; }; -export type RoleCondFormulaValue = { id: string } & ( +export type RoleCondFormulaValue = CondFormulaValueAnd | CondFormulaValueOr | CondFormulaValueNot | CondFormulaValueIsLocal | CondFormulaValueIsRemote | - CondFormulaValueIsSuspended | - CondFormulaValueIsLocked | - CondFormulaValueIsBot | - CondFormulaValueIsCat | - CondFormulaValueIsExplorable | - CondFormulaValueRoleAssignedTo | CondFormulaValueCreatedLessThan | CondFormulaValueCreatedMoreThan | CondFormulaValueFollowersLessThanOrEq | @@ -173,8 +82,7 @@ export type RoleCondFormulaValue = { id: string } & ( CondFormulaValueFollowingLessThanOrEq | CondFormulaValueFollowingMoreThanOrEq | CondFormulaValueNotesLessThanOrEq | - CondFormulaValueNotesMoreThanOrEq -); + CondFormulaValueNotesMoreThanOrEq; @Entity('role') export class MiRole { diff --git a/packages/backend/src/models/RoleAssignment.ts b/packages/backend/src/models/RoleAssignment.ts index 37755d631b..f5ef8dcf69 100644 --- a/packages/backend/src/models/RoleAssignment.ts +++ b/packages/backend/src/models/RoleAssignment.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Signin.ts b/packages/backend/src/models/Signin.ts index f8ff9c57d7..7a46f03b2a 100644 --- a/packages/backend/src/models/Signin.ts +++ b/packages/backend/src/models/Signin.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/SwSubscription.ts b/packages/backend/src/models/SwSubscription.ts index 0c531132b3..092060e09b 100644 --- a/packages/backend/src/models/SwSubscription.ts +++ b/packages/backend/src/models/SwSubscription.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts deleted file mode 100644 index d6c27eae51..0000000000 --- a/packages/backend/src/models/SystemWebhook.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; -import { Serialized } from '@/types.js'; -import { id } from './util/id.js'; - -export const systemWebhookEventTypes = [ - // ユーザからの通報を受けたとき - 'abuseReport', - // 通報を処理したとき - 'abuseReportResolved', - // ユーザが作成された時 - 'userCreated', -] as const; -export type SystemWebhookEventType = typeof systemWebhookEventTypes[number]; - -@Entity('system_webhook') -export class MiSystemWebhook { - @PrimaryColumn(id()) - public id: string; - - /** - * 有効かどうか. - */ - @Index('IDX_system_webhook_isActive', { synchronize: false }) - @Column('boolean', { - default: true, - }) - public isActive: boolean; - - /** - * 更新日時. - */ - @Column('timestamp with time zone', { - default: () => 'CURRENT_TIMESTAMP', - }) - public updatedAt: Date; - - /** - * 最後に送信された日時. - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public latestSentAt: Date | null; - - /** - * 最後に送信されたステータスコード - */ - @Column('integer', { - nullable: true, - }) - public latestStatus: number | null; - - /** - * 通知設定名. - */ - @Column('varchar', { - length: 255, - }) - public name: string; - - /** - * イベント種別. - */ - @Index('IDX_system_webhook_on', { synchronize: false }) - @Column('varchar', { - length: 128, - array: true, - default: '{}', - }) - public on: SystemWebhookEventType[]; - - /** - * Webhook送信先のURL. - */ - @Column('varchar', { - length: 1024, - }) - public url: string; - - /** - * Webhook検証用の値. - */ - @Column('varchar', { - length: 1024, - }) - public secret: string; - - static deserialize(obj: Serialized): MiSystemWebhook { - return { - ...obj, - updatedAt: new Date(obj.updatedAt), - latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null, - }; - } -} diff --git a/packages/backend/src/models/UsedUsername.ts b/packages/backend/src/models/UsedUsername.ts index fbfc126763..ead4cd5313 100644 --- a/packages/backend/src/models/UsedUsername.ts +++ b/packages/backend/src/models/UsedUsername.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index a0bfa21df6..4736f48ab1 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -256,20 +256,6 @@ export class MiUser { }) public token: string | null; - @Index() - @Column('boolean', { - default: true, - comment: 'Whether the User is indexable', - }) - public isIndexable: boolean; - - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the User is sensitive.', - }) - public isSensitive: boolean; - constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/UserGroup.ts b/packages/backend/src/models/UserGroup.ts index 69567c53c6..96005898dd 100644 --- a/packages/backend/src/models/UserGroup.ts +++ b/packages/backend/src/models/UserGroup.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserGroupInvitation.ts b/packages/backend/src/models/UserGroupInvitation.ts index 8d53d63794..59b1aee163 100644 --- a/packages/backend/src/models/UserGroupInvitation.ts +++ b/packages/backend/src/models/UserGroupInvitation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserGroupJoining.ts b/packages/backend/src/models/UserGroupJoining.ts index aa2c26a03d..7cfb6dacb1 100644 --- a/packages/backend/src/models/UserGroupJoining.ts +++ b/packages/backend/src/models/UserGroupJoining.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserIp.ts b/packages/backend/src/models/UserIp.ts index 3e757fcf79..cf01c71122 100644 --- a/packages/backend/src/models/UserIp.ts +++ b/packages/backend/src/models/UserIp.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts index f5252d126c..12c7f28efb 100644 --- a/packages/backend/src/models/UserKeypair.ts +++ b/packages/backend/src/models/UserKeypair.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserList.ts b/packages/backend/src/models/UserList.ts index 5fb991a87d..ed2beb7f4b 100644 --- a/packages/backend/src/models/UserList.ts +++ b/packages/backend/src/models/UserList.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserListFavorite.ts b/packages/backend/src/models/UserListFavorite.ts index 80b2d61eb7..b2c37b8eb4 100644 --- a/packages/backend/src/models/UserListFavorite.ts +++ b/packages/backend/src/models/UserListFavorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserListMembership.ts b/packages/backend/src/models/UserListMembership.ts index af659d071d..1aede8b268 100644 --- a/packages/backend/src/models/UserListMembership.ts +++ b/packages/backend/src/models/UserListMembership.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserMemo.ts b/packages/backend/src/models/UserMemo.ts index 29e28d290a..6d210e7cf2 100644 --- a/packages/backend/src/models/UserMemo.ts +++ b/packages/backend/src/models/UserMemo.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserNotePining.ts b/packages/backend/src/models/UserNotePining.ts index 92c5cd55d0..15379c6144 100644 --- a/packages/backend/src/models/UserNotePining.ts +++ b/packages/backend/src/models/UserNotePining.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserPending.ts b/packages/backend/src/models/UserPending.ts index 99f8a22a84..b488a7f15a 100644 --- a/packages/backend/src/models/UserPending.ts +++ b/packages/backend/src/models/UserPending.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 71b093e6a6..95b59dcb1d 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -171,18 +171,6 @@ export class MiUserProfile { }) public noCrawle: boolean; - @Column('boolean', { - default: true, - comment: 'Whether User is indexable.', - }) - public isIndexable: boolean; - - @Column('boolean', { - default: false, - comment: 'Whether User is sensitive.', - }) - public isSensitive: boolean; - @Column('boolean', { default: true, }) @@ -261,8 +249,6 @@ export class MiUserProfile { type: 'follower'; } | { type: 'mutualFollow'; - } | { - type: 'followingOrFollower'; } | { type: 'list'; userListId: MiUserList['id']; diff --git a/packages/backend/src/models/UserPublickey.ts b/packages/backend/src/models/UserPublickey.ts index 6bcd785304..9c7c05552e 100644 --- a/packages/backend/src/models/UserPublickey.ts +++ b/packages/backend/src/models/UserPublickey.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/UserSecurityKey.ts b/packages/backend/src/models/UserSecurityKey.ts index 0babbe1abe..ace4c9d919 100644 --- a/packages/backend/src/models/UserSecurityKey.ts +++ b/packages/backend/src/models/UserSecurityKey.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts index db24c03b3d..d659c9100a 100644 --- a/packages/backend/src/models/Webhook.ts +++ b/packages/backend/src/models/Webhook.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 47bf1bf34a..dc72c8b598 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -1,18 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm'; -import { DriverUtils } from 'typeorm/driver/DriverUtils.js'; -import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js'; -import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js'; -import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; -import { ObjectUtils } from 'typeorm/util/ObjectUtils.js'; -import { OrmUtils } from 'typeorm/util/OrmUtils.js'; import { MiAbuseReportResolver } from '@/models/AbuseReportResolver.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; -import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAccessToken } from '@/models/AccessToken.js'; import { MiAd } from '@/models/Ad.js'; import { MiAnnouncement } from '@/models/Announcement.js'; @@ -75,7 +67,6 @@ import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiWebhook } from '@/models/Webhook.js'; -import { MiSystemWebhook } from '@/models/SystemWebhook.js'; import { MiChannel } from '@/models/Channel.js'; import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; import { MiRole } from '@/models/Role.js'; @@ -83,56 +74,11 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js'; import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; -import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; -import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; - -export interface MiRepository { - createTableColumnNames(this: Repository & MiRepository): string[]; - insertOne(this: Repository & MiRepository, entity: QueryDeepPartialEntity, findOptions?: Pick, 'relations'>): Promise; - selectAliasColumnNames(this: Repository & MiRepository, queryBuilder: InsertQueryBuilder, builder: SelectQueryBuilder): void; -} - -export const miRepository = { - createTableColumnNames() { - return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName); - }, - async insertOne(entity, findOptions?) { - const queryBuilder = this.createQueryBuilder().insert().values(entity); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const mainAlias = queryBuilder.expressionMap.mainAlias!; - const name = mainAlias.name; - mainAlias.name = 't'; - const columnNames = this.createTableColumnNames(); - queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2)); - const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - builder.expressionMap.mainAlias!.tablePath = 'cte'; - this.selectAliasColumnNames(queryBuilder, builder); - if (findOptions) { - builder.setFindOptions(findOptions); - } - const raw = await builder.execute(); - mainAlias.name = name; - const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw); - const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw); - const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias); - return result[0]; - }, - selectAliasColumnNames(queryBuilder, builder) { - let selectOrAddSelect = (selection: string, selectionAliasName?: string) => { - selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName); - return builder.select(selection, selectionAliasName); - }; - for (const columnName of this.createTableColumnNames()) { - selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`); - } - }, -} satisfies MiRepository; +import type { Repository } from 'typeorm'; export { MiAbuseReportResolver, MiAbuseUserReport, - MiAbuseReportNotificationRecipient, MiAccessToken, MiAd, MiAnnouncement, @@ -195,7 +141,6 @@ export { MiUserPublickey, MiUserSecurityKey, MiWebhook, - MiSystemWebhook, MiChannel, MiRetentionAggregation, MiRole, @@ -203,80 +148,76 @@ export { MiFlash, MiFlashLike, MiUserMemo, - MiBubbleGameRecord, }; -export type AbuseReportResolversRepository = Repository & MiRepository; -export type AbuseUserReportsRepository = Repository & MiRepository; -export type AbuseReportNotificationRecipientRepository = Repository & MiRepository; -export type AccessTokensRepository = Repository & MiRepository; -export type AdsRepository = Repository & MiRepository; -export type AnnouncementsRepository = Repository & MiRepository; -export type AnnouncementReadsRepository = Repository & MiRepository; -export type AntennasRepository = Repository & MiRepository; -export type AppsRepository = Repository & MiRepository; -export type AvatarDecorationsRepository = Repository & MiRepository; -export type AuthSessionsRepository = Repository & MiRepository; -export type BlockingsRepository = Repository & MiRepository; -export type ChannelFollowingsRepository = Repository & MiRepository; -export type ChannelFavoritesRepository = Repository & MiRepository; -export type ClipsRepository = Repository & MiRepository; -export type ClipNotesRepository = Repository & MiRepository; -export type ClipFavoritesRepository = Repository & MiRepository; -export type DriveFilesRepository = Repository & MiRepository; -export type DriveFoldersRepository = Repository & MiRepository; -export type EmojisRepository = Repository & MiRepository; -export type EventsRepository = Repository & MiRepository; -export type FollowingsRepository = Repository & MiRepository; -export type FollowRequestsRepository = Repository & MiRepository; -export type GalleryLikesRepository = Repository & MiRepository; -export type GalleryPostsRepository = Repository & MiRepository; -export type HashtagsRepository = Repository & MiRepository; -export type InstancesRepository = Repository & MiRepository; -export type MessagingMessagesRepository = Repository & MiRepository; -export type MetasRepository = Repository & MiRepository; -export type ModerationLogsRepository = Repository & MiRepository; -export type MutingsRepository = Repository & MiRepository; -export type RenoteMutingsRepository = Repository & MiRepository; -export type NotesRepository = Repository & MiRepository; -export type NoteFavoritesRepository = Repository & MiRepository; -export type NoteReactionsRepository = Repository & MiRepository; -export type NoteThreadMutingsRepository = Repository & MiRepository; -export type NoteUnreadsRepository = Repository & MiRepository; -export type PagesRepository = Repository & MiRepository; -export type PageLikesRepository = Repository & MiRepository; -export type PasswordResetRequestsRepository = Repository & MiRepository; -export type PollsRepository = Repository & MiRepository; -export type PollVotesRepository = Repository & MiRepository; -export type PromoNotesRepository = Repository & MiRepository; -export type PromoReadsRepository = Repository & MiRepository; -export type RegistrationTicketsRepository = Repository & MiRepository; -export type RegistryItemsRepository = Repository & MiRepository; -export type RelaysRepository = Repository & MiRepository; -export type SigninsRepository = Repository & MiRepository; -export type SwSubscriptionsRepository = Repository & MiRepository; -export type UsedUsernamesRepository = Repository & MiRepository; -export type UsersRepository = Repository & MiRepository; -export type UserGroupsRepository = Repository & MiRepository; -export type UserGroupInvitationsRepository = Repository & MiRepository; -export type UserGroupJoiningsRepository = Repository & MiRepository; -export type UserIpsRepository = Repository & MiRepository; -export type UserKeypairsRepository = Repository & MiRepository; -export type UserListsRepository = Repository & MiRepository; -export type UserListFavoritesRepository = Repository & MiRepository; -export type UserListMembershipsRepository = Repository & MiRepository; -export type UserNotePiningsRepository = Repository & MiRepository; -export type UserPendingsRepository = Repository & MiRepository; -export type UserProfilesRepository = Repository & MiRepository; -export type UserPublickeysRepository = Repository & MiRepository; -export type UserSecurityKeysRepository = Repository & MiRepository; -export type WebhooksRepository = Repository & MiRepository; -export type SystemWebhooksRepository = Repository & MiRepository; -export type ChannelsRepository = Repository & MiRepository; -export type RetentionAggregationsRepository = Repository & MiRepository; -export type RolesRepository = Repository & MiRepository; -export type RoleAssignmentsRepository = Repository & MiRepository; -export type FlashsRepository = Repository & MiRepository; -export type FlashLikesRepository = Repository & MiRepository; -export type UserMemoRepository = Repository & MiRepository; -export type BubbleGameRecordsRepository = Repository & MiRepository; +export type AbuseReportResolversRepository = Repository; +export type AbuseUserReportsRepository = Repository; +export type AccessTokensRepository = Repository; +export type AdsRepository = Repository; +export type AnnouncementsRepository = Repository; +export type AnnouncementReadsRepository = Repository; +export type AntennasRepository = Repository; +export type AppsRepository = Repository; +export type AvatarDecorationsRepository = Repository; +export type AuthSessionsRepository = Repository; +export type BlockingsRepository = Repository; +export type ChannelFollowingsRepository = Repository; +export type ChannelFavoritesRepository = Repository; +export type ClipsRepository = Repository; +export type ClipNotesRepository = Repository; +export type ClipFavoritesRepository = Repository; +export type DriveFilesRepository = Repository; +export type DriveFoldersRepository = Repository; +export type EmojisRepository = Repository; +export type EventsRepository = Repository; +export type FollowingsRepository = Repository; +export type FollowRequestsRepository = Repository; +export type GalleryLikesRepository = Repository; +export type GalleryPostsRepository = Repository; +export type HashtagsRepository = Repository; +export type InstancesRepository = Repository; +export type MessagingMessagesRepository = Repository; +export type MetasRepository = Repository; +export type ModerationLogsRepository = Repository; +export type MutingsRepository = Repository; +export type RenoteMutingsRepository = Repository; +export type NotesRepository = Repository; +export type NoteFavoritesRepository = Repository; +export type NoteReactionsRepository = Repository; +export type NoteThreadMutingsRepository = Repository; +export type NoteUnreadsRepository = Repository; +export type PagesRepository = Repository; +export type PageLikesRepository = Repository; +export type PasswordResetRequestsRepository = Repository; +export type PollsRepository = Repository; +export type PollVotesRepository = Repository; +export type PromoNotesRepository = Repository; +export type PromoReadsRepository = Repository; +export type RegistrationTicketsRepository = Repository; +export type RegistryItemsRepository = Repository; +export type RelaysRepository = Repository; +export type SigninsRepository = Repository; +export type SwSubscriptionsRepository = Repository; +export type UsedUsernamesRepository = Repository; +export type UsersRepository = Repository; +export type UserGroupsRepository = Repository; +export type UserGroupInvitationsRepository = Repository; +export type UserGroupJoiningsRepository = Repository; +export type UserIpsRepository = Repository; +export type UserKeypairsRepository = Repository; +export type UserListsRepository = Repository; +export type UserListFavoritesRepository = Repository; +export type UserListMembershipsRepository = Repository; +export type UserNotePiningsRepository = Repository; +export type UserPendingsRepository = Repository; +export type UserProfilesRepository = Repository; +export type UserPublickeysRepository = Repository; +export type UserSecurityKeysRepository = Repository; +export type WebhooksRepository = Repository; +export type ChannelsRepository = Repository; +export type RetentionAggregationsRepository = Repository; +export type RolesRepository = Repository; +export type RoleAssignmentsRepository = Repository; +export type FlashsRepository = Repository; +export type FlashLikesRepository = Repository; +export type UserMemoRepository = Repository; diff --git a/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts deleted file mode 100644 index 6215f0f5a2..0000000000 --- a/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedAbuseReportNotificationRecipientSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - isActive: { - type: 'boolean', - optional: false, nullable: false, - }, - updatedAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - method: { - type: 'string', - optional: false, nullable: false, - enum: ['email', 'webhook'], - }, - userId: { - type: 'string', - optional: true, nullable: false, - }, - user: { - type: 'object', - optional: true, nullable: false, - ref: 'UserLite', - }, - systemWebhookId: { - type: 'string', - optional: true, nullable: false, - }, - systemWebhook: { - type: 'object', - optional: true, nullable: false, - ref: 'SystemWebhook', - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts index b01b39a38b..d139e5a72e 100644 --- a/packages/backend/src/models/json-schema/ad.ts +++ b/packages/backend/src/models/json-schema/ad.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/announcement.ts b/packages/backend/src/models/json-schema/announcement.ts index b9352bd31e..ca3fba9772 100644 --- a/packages/backend/src/models/json-schema/announcement.ts +++ b/packages/backend/src/models/json-schema/announcement.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -37,12 +37,10 @@ export const packedAnnouncementSchema = { icon: { type: 'string', optional: false, nullable: false, - enum: ['info', 'warning', 'error', 'success'], }, display: { type: 'string', optional: false, nullable: false, - enum: ['dialog', 'normal', 'banner'], }, needConfirmationToRead: { type: 'boolean', diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index 9e5d84f2ed..f3b4face1c 100644 --- a/packages/backend/src/models/json-schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -77,10 +77,9 @@ export const packedAntennaSchema = { optional: false, nullable: false, default: false, }, - excludeBots: { + notify: { type: 'boolean', optional: false, nullable: false, - default: false, }, withReplies: { type: 'boolean', @@ -100,10 +99,5 @@ export const packedAntennaSchema = { optional: false, nullable: false, default: false, }, - notify: { - type: 'boolean', - optional: false, nullable: false, - default: false, - }, }, } as const; diff --git a/packages/backend/src/models/json-schema/app.ts b/packages/backend/src/models/json-schema/app.ts index 6148232224..3c9886c836 100644 --- a/packages/backend/src/models/json-schema/app.ts +++ b/packages/backend/src/models/json-schema/app.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/blocking.ts b/packages/backend/src/models/json-schema/blocking.ts index 2d02ba6a70..d5dd0a9683 100644 --- a/packages/backend/src/models/json-schema/blocking.ts +++ b/packages/backend/src/models/json-schema/blocking.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -25,7 +25,7 @@ export const packedBlockingSchema = { blockee: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, } as const; diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts index d233f7858d..f490b50300 100644 --- a/packages/backend/src/models/json-schema/channel.ts +++ b/packages/backend/src/models/json-schema/channel.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/clip.ts b/packages/backend/src/models/json-schema/clip.ts index c4e7055cd8..721a479878 100644 --- a/packages/backend/src/models/json-schema/clip.ts +++ b/packages/backend/src/models/json-schema/clip.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -52,9 +52,5 @@ export const packedClipSchema = { type: 'boolean', optional: true, nullable: false, }, - notesCount: { - type: 'integer', - optional: true, nullable: false, - }, }, } as const; diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index 5ee1561c50..cf8233e70a 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -20,7 +20,7 @@ export const packedDriveFileSchema = { name: { type: 'string', optional: false, nullable: false, - example: '192.jpg', + example: 'lenna.jpg', }, type: { type: 'string', diff --git a/packages/backend/src/models/json-schema/drive-folder.ts b/packages/backend/src/models/json-schema/drive-folder.ts index 12012a7e12..d50cef86a9 100644 --- a/packages/backend/src/models/json-schema/drive-folder.ts +++ b/packages/backend/src/models/json-schema/drive-folder.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 62686ad5ae..0e9a76c861 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -27,10 +27,6 @@ export const packedEmojiSimpleSchema = { type: 'string', optional: false, nullable: false, }, - localOnly: { - type: 'boolean', - optional: true, nullable: false, - }, isSensitive: { type: 'boolean', optional: true, nullable: false, diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 3488ce250d..575e14197a 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -45,11 +45,6 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, - suspensionState: { - type: 'string', - nullable: false, optional: false, - enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'], - }, isBlocked: { type: 'boolean', optional: false, nullable: false, @@ -88,10 +83,6 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, - isMediaSilenced: { - type: 'boolean', - optional: false, nullable: false, - }, iconUrl: { type: 'string', optional: false, nullable: true, @@ -116,9 +107,5 @@ export const packedFederationInstanceSchema = { optional: false, nullable: true, format: 'date-time', }, - moderationNote: { - type: 'string', - optional: true, nullable: true, - }, }, } as const; diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 952df649ad..120a7dcc05 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/following.ts b/packages/backend/src/models/json-schema/following.ts index c5295a5128..7a5b0e5460 100644 --- a/packages/backend/src/models/json-schema/following.ts +++ b/packages/backend/src/models/json-schema/following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -30,12 +30,12 @@ export const packedFollowingSchema = { followee: { type: 'object', optional: true, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, follower: { type: 'object', optional: true, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, } as const; diff --git a/packages/backend/src/models/json-schema/gallery-post.ts b/packages/backend/src/models/json-schema/gallery-post.ts index a46d5115c2..ca1deec55c 100644 --- a/packages/backend/src/models/json-schema/gallery-post.ts +++ b/packages/backend/src/models/json-schema/gallery-post.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/hashtag.ts b/packages/backend/src/models/json-schema/hashtag.ts index 3f20cbea82..56a8ecc0b3 100644 --- a/packages/backend/src/models/json-schema/hashtag.ts +++ b/packages/backend/src/models/json-schema/hashtag.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/invite-code.ts b/packages/backend/src/models/json-schema/invite-code.ts index 08d1b8fd0c..1bf9a54c9b 100644 --- a/packages/backend/src/models/json-schema/invite-code.ts +++ b/packages/backend/src/models/json-schema/invite-code.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/messaging-message.ts b/packages/backend/src/models/json-schema/messaging-message.ts index b8cf33e3bd..74caa0ed76 100644 --- a/packages/backend/src/models/json-schema/messaging-message.ts +++ b/packages/backend/src/models/json-schema/messaging-message.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts deleted file mode 100644 index 501bb807d6..0000000000 --- a/packages/backend/src/models/json-schema/meta.ts +++ /dev/null @@ -1,350 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedMetaLiteSchema = { - type: 'object', - optional: false, nullable: false, - properties: { - maintainerName: { - type: 'string', - optional: false, nullable: true, - }, - maintainerEmail: { - type: 'string', - optional: false, nullable: true, - }, - version: { - type: 'string', - optional: false, nullable: false, - }, - basedMisskeyVersion: { - type: 'string', - optional: false, nullable: false, - }, - providesTarball: { - type: 'boolean', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: true, - }, - shortName: { - type: 'string', - optional: false, nullable: true, - }, - uri: { - type: 'string', - optional: false, nullable: false, - format: 'url', - example: 'https://cherrypick.example.com', - }, - description: { - type: 'string', - optional: false, nullable: true, - }, - langs: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - tosUrl: { - type: 'string', - optional: false, nullable: true, - }, - repositoryUrl: { - type: 'string', - optional: false, nullable: true, - default: 'https://github.com/kokonect-link/cherrypick', - }, - feedbackUrl: { - type: 'string', - optional: false, nullable: true, - default: 'https://github.com/kokonect-link/cherrypick/issues/new', - }, - defaultDarkTheme: { - type: 'string', - optional: false, nullable: true, - }, - defaultLightTheme: { - type: 'string', - optional: false, nullable: true, - }, - disableRegistration: { - type: 'boolean', - optional: false, nullable: false, - }, - emailRequiredForSignup: { - type: 'boolean', - optional: false, nullable: false, - }, - enableHcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - hcaptchaSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - enableMcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - mcaptchaSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - mcaptchaInstanceUrl: { - type: 'string', - optional: false, nullable: true, - }, - enableRecaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - recaptchaSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - enableTurnstile: { - type: 'boolean', - optional: false, nullable: false, - }, - turnstileSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - swPublickey: { - type: 'string', - optional: false, nullable: true, - }, - mascotImageUrl: { - type: 'string', - optional: false, nullable: false, - default: '/assets/ai.png', - }, - bannerUrl: { - type: 'string', - optional: false, nullable: true, - }, - serverErrorImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - infoImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - notFoundImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - iconUrl: { - type: 'string', - optional: false, nullable: true, - }, - maxNoteTextLength: { - type: 'number', - optional: false, nullable: false, - }, - ads: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - url: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - place: { - type: 'string', - optional: false, nullable: false, - }, - ratio: { - type: 'number', - optional: false, nullable: false, - }, - imageUrl: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - dayOfWeek: { - type: 'integer', - optional: false, nullable: false, - }, - }, - }, - }, - notesPerOneAd: { - type: 'number', - optional: false, nullable: false, - default: 0, - }, - enableEmail: { - type: 'boolean', - optional: false, nullable: false, - }, - enableServiceWorker: { - type: 'boolean', - optional: false, nullable: false, - }, - translatorAvailable: { - type: 'boolean', - optional: false, nullable: false, - }, - mediaProxy: { - type: 'string', - optional: false, nullable: false, - }, - enableUrlPreview: { - type: 'boolean', - optional: false, nullable: false, - }, - urlPreviewEndpoint: { - type: 'string', - optional: false, nullable: false, - }, - backgroundImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - impressumUrl: { - type: 'string', - optional: false, nullable: true, - }, - logoImageUrl: { - type: 'string', - optional: false, nullable: true, - }, - privacyPolicyUrl: { - type: 'string', - optional: false, nullable: true, - }, - inquiryUrl: { - type: 'string', - optional: false, nullable: true, - }, - serverRules: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - }, - }, - themeColor: { - type: 'string', - optional: false, nullable: true, - }, - policies: { - type: 'object', - optional: false, nullable: false, - ref: 'RolePolicies', - }, - noteSearchableScope: { - type: 'string', - enum: ['local', 'global'], - optional: false, nullable: false, - default: 'local', - }, - }, -} as const; - -export const packedMetaDetailedOnlySchema = { - type: 'object', - optional: false, nullable: false, - properties: { - features: { - type: 'object', - optional: true, nullable: false, - properties: { - registration: { - type: 'boolean', - optional: false, nullable: false, - }, - emailRequiredForSignup: { - type: 'boolean', - optional: false, nullable: false, - }, - localTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - globalTimeline: { - type: 'boolean', - optional: false, nullable: false, - }, - hcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - turnstile: { - type: 'boolean', - optional: false, nullable: false, - }, - recaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - objectStorage: { - type: 'boolean', - optional: false, nullable: false, - }, - serviceWorker: { - type: 'boolean', - optional: false, nullable: false, - }, - miauth: { - type: 'boolean', - optional: true, nullable: false, - default: true, - }, - }, - }, - proxyAccountName: { - type: 'string', - optional: false, nullable: true, - }, - requireSetup: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - cacheRemoteFiles: { - type: 'boolean', - optional: false, nullable: false, - }, - cacheRemoteSensitiveFiles: { - type: 'boolean', - optional: false, nullable: false, - }, - }, -} as const; - -export const packedMetaDetailedSchema = { - type: 'object', - allOf: [ - { - type: 'object', - ref: 'MetaLite', - }, - { - type: 'object', - ref: 'MetaDetailedOnly', - }, - ], -} as const; diff --git a/packages/backend/src/models/json-schema/muting.ts b/packages/backend/src/models/json-schema/muting.ts index b5fab013ef..886aabbff5 100644 --- a/packages/backend/src/models/json-schema/muting.ts +++ b/packages/backend/src/models/json-schema/muting.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -30,7 +30,7 @@ export const packedMutingSchema = { mutee: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, } as const; diff --git a/packages/backend/src/models/json-schema/note-favorite.ts b/packages/backend/src/models/json-schema/note-favorite.ts index d2a3745f4b..337db9c43b 100644 --- a/packages/backend/src/models/json-schema/note-favorite.ts +++ b/packages/backend/src/models/json-schema/note-favorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/note-reaction.ts b/packages/backend/src/models/json-schema/note-reaction.ts index 95658ace1f..1034559ac1 100644 --- a/packages/backend/src/models/json-schema/note-reaction.ts +++ b/packages/backend/src/models/json-schema/note-reaction.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 01a406b1eb..cfc0b76cbf 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -91,7 +91,6 @@ export const packedNoteSchema = { visibility: { type: 'string', optional: false, nullable: false, - enum: ['public', 'home', 'followers', 'specified', 'private'], }, mentions: { type: 'array', @@ -140,53 +139,6 @@ export const packedNoteSchema = { poll: { type: 'object', optional: true, nullable: true, - properties: { - expiresAt: { - type: 'string', - optional: true, nullable: true, - format: 'date-time', - }, - multiple: { - type: 'boolean', - optional: false, nullable: false, - }, - choices: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - isVoted: { - type: 'boolean', - optional: false, nullable: false, - }, - text: { - type: 'string', - optional: false, nullable: false, - }, - votes: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, - }, - }, - }, - deleteAt: { - type: 'string', - optional: true, nullable: true, - format: 'date-time', - }, - emojis: { - type: 'object', - optional: true, nullable: false, - additionalProperties: { - anyOf: [{ - type: 'string', - }], - }, }, event: { type: 'object', @@ -222,10 +174,6 @@ export const packedNoteSchema = { type: 'boolean', optional: false, nullable: false, }, - userId: { - type: 'string', - optional: false, nullable: true, - }, }, }, localOnly: { @@ -235,29 +183,10 @@ export const packedNoteSchema = { reactionAcceptance: { type: 'string', optional: false, nullable: true, - enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], - }, - reactionEmojis: { - type: 'object', - optional: false, nullable: false, - additionalProperties: { - anyOf: [{ - type: 'string', - }], - }, }, reactions: { type: 'object', optional: false, nullable: false, - additionalProperties: { - anyOf: [{ - type: 'number', - }], - }, - }, - reactionCount: { - type: 'number', - optional: false, nullable: false, }, renoteCount: { type: 'number', @@ -289,7 +218,7 @@ export const packedNoteSchema = { }, myReaction: { - type: 'string', + type: 'object', optional: true, nullable: true, }, }, diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 052eb84bf7..b8faea4903 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { notificationTypes } from '@/types.js'; -const baseSchema = { +export const packedNotificationSchema = { type: 'object', properties: { id: { @@ -23,383 +23,68 @@ const baseSchema = { optional: false, nullable: false, enum: [...notificationTypes, 'reaction:grouped', 'renote:grouped'], }, - }, -} as const; - -export const packedNotificationSchema = { - type: 'object', - oneOf: [{ - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['note'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['mention'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['reply'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, + user: { + type: 'object', + ref: 'UserLite', + optional: true, nullable: true, }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['renote'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['quote'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['reaction'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - reaction: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['pollEnded'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['follow'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, + userId: { + type: 'string', + optional: true, nullable: true, + format: 'id', }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['receiveFollowRequest'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, + note: { + type: 'object', + ref: 'Note', + optional: true, nullable: true, }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['followRequestAccepted'], - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, + reaction: { + type: 'string', + optional: true, nullable: true, }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['roleAssigned'], - }, - role: { - type: 'object', - ref: 'Role', - optional: false, nullable: false, - }, + achievement: { + type: 'string', + optional: true, nullable: false, }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['achievementEarned'], - }, - achievement: { - type: 'string', - optional: false, nullable: false, - }, + body: { + type: 'string', + optional: true, nullable: true, }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['app'], - }, - body: { - type: 'string', - optional: false, nullable: false, - }, - header: { - type: 'string', - optional: false, nullable: false, - }, - icon: { - type: 'string', - optional: false, nullable: false, - }, + header: { + type: 'string', + optional: true, nullable: true, }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['reaction:grouped'], - }, - note: { - type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - reactions: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - properties: { - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - reaction: { - type: 'string', - optional: false, nullable: false, - }, + icon: { + type: 'string', + optional: true, nullable: true, + }, + reactions: { + type: 'array', + optional: true, nullable: true, + items: { + type: 'object', + properties: { + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + reaction: { + type: 'string', + optional: false, nullable: false, }, - required: ['user', 'reaction'], }, + required: ['user', 'reaction'], }, }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['renote:grouped'], - }, - note: { + users: { + type: 'array', + optional: true, nullable: true, + items: { type: 'object', - ref: 'Note', - optional: false, nullable: false, - }, - users: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['test'], - }, - }, - }, { - type: 'object', - properties: { - ...baseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['groupInvited'], - }, - invitation: { - type: 'string', + ref: 'UserLite', optional: false, nullable: false, - format: 'id', }, }, - }], + }, } as const; diff --git a/packages/backend/src/models/json-schema/page.ts b/packages/backend/src/models/json-schema/page.ts index 748d6f1245..525af42928 100644 --- a/packages/backend/src/models/json-schema/page.ts +++ b/packages/backend/src/models/json-schema/page.ts @@ -1,110 +1,8 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -const blockBaseSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - type: { - type: 'string', - optional: false, nullable: false, - }, - }, -} as const; - -const textBlockSchema = { - type: 'object', - properties: { - ...blockBaseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['text'], - }, - text: { - type: 'string', - optional: false, nullable: false, - }, - }, -} as const; - -const sectionBlockSchema = { - type: 'object', - properties: { - ...blockBaseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['section'], - }, - title: { - type: 'string', - optional: false, nullable: false, - }, - children: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'PageBlock', - selfRef: true, - }, - }, - }, -} as const; - -const imageBlockSchema = { - type: 'object', - properties: { - ...blockBaseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['image'], - }, - fileId: { - type: 'string', - optional: false, nullable: true, - }, - }, -} as const; - -const noteBlockSchema = { - type: 'object', - properties: { - ...blockBaseSchema.properties, - type: { - type: 'string', - optional: false, nullable: false, - enum: ['note'], - }, - detailed: { - type: 'boolean', - optional: false, nullable: false, - }, - note: { - type: 'string', - optional: false, nullable: true, - }, - }, -} as const; - -export const packedPageBlockSchema = { - type: 'object', - oneOf: [ - textBlockSchema, - sectionBlockSchema, - imageBlockSchema, - noteBlockSchema, - ], -} as const; - export const packedPageSchema = { type: 'object', properties: { @@ -140,7 +38,6 @@ export const packedPageSchema = { items: { type: 'object', optional: false, nullable: false, - ref: 'PageBlock', }, }, variables: { diff --git a/packages/backend/src/models/json-schema/queue.ts b/packages/backend/src/models/json-schema/queue.ts index 2ecf5c831f..2bf19d7d69 100644 --- a/packages/backend/src/models/json-schema/queue.ts +++ b/packages/backend/src/models/json-schema/queue.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/renote-muting.ts b/packages/backend/src/models/json-schema/renote-muting.ts index 344d6c7c00..e54ccfda15 100644 --- a/packages/backend/src/models/json-schema/renote-muting.ts +++ b/packages/backend/src/models/json-schema/renote-muting.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -25,7 +25,7 @@ export const packedRenoteMutingSchema = { mutee: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, } as const; diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 6fd4c26dec..b0c6804bb8 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -1,286 +1,23 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedRoleCondFormulaLogicsSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['and', 'or'], - }, - values: { - type: 'array', - nullable: false, optional: false, - items: { - ref: 'RoleCondFormulaValue', - }, - }, - }, -} as const; - -export const packedRoleCondFormulaValueNot = { +const rolePolicyValue = { type: 'object', properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['not'], - }, value: { - type: 'object', - optional: false, - ref: 'RoleCondFormulaValue', - }, - }, -} as const; - -export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['isLocal', 'isRemote'], - }, - }, -} as const; - -export const packedRoleCondFormulaValueUserSettingBooleanSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['isSuspended', 'isLocked', 'isBot', 'isCat', 'isExplorable'], - }, - }, -} as const; - -export const packedRoleCondFormulaValueAssignedRoleSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: ['roleAssignedTo'], - }, - roleId: { - type: 'string', - nullable: false, optional: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - }, -} as const; - -export const packedRoleCondFormulaValueCreatedSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: [ - 'createdLessThan', - 'createdMoreThan', - ], - }, - sec: { - type: 'number', - nullable: false, optional: false, - }, - }, -} as const; - -export const packedRoleCondFormulaFollowersOrFollowingOrNotesSchema = { - type: 'object', - properties: { - id: { - type: 'string', optional: false, - }, - type: { - type: 'string', - nullable: false, optional: false, - enum: [ - 'followersLessThanOrEq', - 'followersMoreThanOrEq', - 'followingLessThanOrEq', - 'followingMoreThanOrEq', - 'notesLessThanOrEq', - 'notesMoreThanOrEq', + oneOf: [ + { + type: 'integer', + optional: false, nullable: false, + }, + { + type: 'boolean', + optional: false, nullable: false, + }, ], }, - value: { - type: 'number', - nullable: false, optional: false, - }, - }, -} as const; - -export const packedRoleCondFormulaValueSchema = { - type: 'object', - oneOf: [ - { - ref: 'RoleCondFormulaLogics', - }, - { - ref: 'RoleCondFormulaValueNot', - }, - { - ref: 'RoleCondFormulaValueIsLocalOrRemote', - }, - { - ref: 'RoleCondFormulaValueUserSettingBooleanSchema', - }, - { - ref: 'RoleCondFormulaValueAssignedRole', - }, - { - ref: 'RoleCondFormulaValueCreated', - }, - { - ref: 'RoleCondFormulaFollowersOrFollowingOrNotes', - }, - ], -} as const; - -export const packedRolePoliciesSchema = { - type: 'object', - optional: false, nullable: false, - properties: { - gtlAvailable: { - type: 'boolean', - optional: false, nullable: false, - }, - ltlAvailable: { - type: 'boolean', - optional: false, nullable: false, - }, - canPublicNote: { - type: 'boolean', - optional: false, nullable: false, - }, - mentionLimit: { - type: 'integer', - optional: false, nullable: false, - }, - canInvite: { - type: 'boolean', - optional: false, nullable: false, - }, - inviteLimit: { - type: 'integer', - optional: false, nullable: false, - }, - inviteLimitCycle: { - type: 'integer', - optional: false, nullable: false, - }, - inviteExpirationTime: { - type: 'integer', - optional: false, nullable: false, - }, - canManageCustomEmojis: { - type: 'boolean', - optional: false, nullable: false, - }, - canManageAvatarDecorations: { - type: 'boolean', - optional: false, nullable: false, - }, - canSearchNotes: { - type: 'boolean', - optional: false, nullable: false, - }, - canAdvancedSearchNotes: { - type: 'boolean', - optional: false, nullable: false, - }, - canUseTranslator: { - type: 'boolean', - optional: false, nullable: false, - }, - canHideAds: { - type: 'boolean', - optional: false, nullable: false, - }, - driveCapacityMb: { - type: 'integer', - optional: false, nullable: false, - }, - alwaysMarkNsfw: { - type: 'boolean', - optional: false, nullable: false, - }, - canUpdateBioMedia: { - type: 'boolean', - optional: false, nullable: false, - }, - pinLimit: { - type: 'integer', - optional: false, nullable: false, - }, - antennaLimit: { - type: 'integer', - optional: false, nullable: false, - }, - wordMuteLimit: { - type: 'integer', - optional: false, nullable: false, - }, - webhookLimit: { - type: 'integer', - optional: false, nullable: false, - }, - clipLimit: { - type: 'integer', - optional: false, nullable: false, - }, - noteEachClipsLimit: { - type: 'integer', - optional: false, nullable: false, - }, - userListLimit: { - type: 'integer', - optional: false, nullable: false, - }, - userEachUserListsLimit: { - type: 'integer', - optional: false, nullable: false, - }, - rateLimitFactor: { - type: 'integer', - optional: false, nullable: false, - }, - avatarDecorationLimit: { - type: 'integer', - optional: false, nullable: false, - }, - fileSizeLimit: { + priority: { type: 'integer', optional: false, nullable: false, }, - canEditNote: { + useDefault: { type: 'boolean', optional: false, nullable: false, }, @@ -360,7 +97,6 @@ export const packedRoleSchema = { condFormula: { type: 'object', optional: false, nullable: false, - ref: 'RoleCondFormulaValue', }, isPublic: { type: 'boolean', @@ -385,28 +121,31 @@ export const packedRoleSchema = { policies: { type: 'object', optional: false, nullable: false, - additionalProperties: { - anyOf: [{ - type: 'object', - properties: { - value: { - oneOf: [ - { - type: 'integer', - }, - { - type: 'boolean', - }, - ], - }, - priority: { - type: 'integer', - }, - useDefault: { - type: 'boolean', - }, - }, - }], + properties: { + pinLimit: rolePolicyValue, + canInvite: rolePolicyValue, + clipLimit: rolePolicyValue, + canHideAds: rolePolicyValue, + inviteLimit: rolePolicyValue, + antennaLimit: rolePolicyValue, + gtlAvailable: rolePolicyValue, + ltlAvailable: rolePolicyValue, + webhookLimit: rolePolicyValue, + canPublicNote: rolePolicyValue, + userListLimit: rolePolicyValue, + wordMuteLimit: rolePolicyValue, + alwaysMarkNsfw: rolePolicyValue, + canSearchNotes: rolePolicyValue, + driveCapacityMb: rolePolicyValue, + rateLimitFactor: rolePolicyValue, + inviteLimitCycle: rolePolicyValue, + noteEachClipsLimit: rolePolicyValue, + inviteExpirationTime: rolePolicyValue, + canManageCustomEmojis: rolePolicyValue, + userEachUserListsLimit: rolePolicyValue, + canManageAvatarDecorations: rolePolicyValue, + canUseTranslator: rolePolicyValue, + avatarDecorationLimit: rolePolicyValue, }, }, usersCount: { diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts index 45732a742b..d27d2490c5 100644 --- a/packages/backend/src/models/json-schema/signin.ts +++ b/packages/backend/src/models/json-schema/signin.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - export const packedSigninSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/system-webhook.ts b/packages/backend/src/models/json-schema/system-webhook.ts deleted file mode 100644 index d83065a743..0000000000 --- a/packages/backend/src/models/json-schema/system-webhook.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; - -export const packedSystemWebhookSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - isActive: { - type: 'boolean', - optional: false, nullable: false, - }, - updatedAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: false, - }, - latestSentAt: { - type: 'string', - format: 'date-time', - optional: false, nullable: true, - }, - latestStatus: { - type: 'number', - optional: false, nullable: true, - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - on: { - type: 'array', - items: { - type: 'string', - optional: false, nullable: false, - enum: systemWebhookEventTypes, - }, - }, - url: { - type: 'string', - optional: false, nullable: false, - }, - secret: { - type: 'string', - optional: false, nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/json-schema/user-group.ts b/packages/backend/src/models/json-schema/user-group.ts index f0e81928db..63eeeb59d3 100644 --- a/packages/backend/src/models/json-schema/user-group.ts +++ b/packages/backend/src/models/json-schema/user-group.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/user-list.ts b/packages/backend/src/models/json-schema/user-list.ts index dc9af25602..6d9ed6883b 100644 --- a/packages/backend/src/models/json-schema/user-list.ts +++ b/packages/backend/src/models/json-schema/user-list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index ef5695040e..b712d3448e 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -1,40 +1,18 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -export const notificationRecieveConfig = { +const notificationRecieveConfig = { type: 'object', - oneOf: [ - { - type: 'object', - nullable: false, - properties: { - type: { - type: 'string', - nullable: false, - enum: ['all', 'following', 'follower', 'mutualFollow', 'followingOrFollower', 'never'], - }, - }, - required: ['type'], - }, - { - type: 'object', - nullable: false, - properties: { - type: { - type: 'string', - nullable: false, - enum: ['list'], - }, - userListId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['type', 'userListId'], + nullable: false, optional: true, + properties: { + type: { + type: 'string', + nullable: false, optional: false, + enum: ['all', 'following', 'follower', 'mutualFollow', 'list', 'never'], }, - ], + }, } as const; export const packedUserLiteSchema = { @@ -156,9 +134,6 @@ export const packedUserLiteSchema = { emojis: { type: 'object', nullable: false, optional: false, - additionalProperties: { - type: 'string', - }, }, onlineStatus: { type: 'string', @@ -254,11 +229,6 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: false, optional: false, example: false, }, - isSensitive: { - type: 'boolean', - nullable: false, optional: false, - example: false, - }, description: { type: 'string', nullable: true, optional: false, @@ -494,14 +464,6 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, - isIndexable: { - type: 'boolean', - nullable: false, optional: false, - }, - isSensitive: { - type: 'boolean', - nullable: false, optional: false, - }, isDeleted: { type: 'boolean', nullable: false, optional: false, @@ -596,21 +558,16 @@ export const packedMeDetailedOnlySchema = { type: 'object', nullable: false, optional: false, properties: { - note: { optional: true, ...notificationRecieveConfig }, - follow: { optional: true, ...notificationRecieveConfig }, - mention: { optional: true, ...notificationRecieveConfig }, - reply: { optional: true, ...notificationRecieveConfig }, - renote: { optional: true, ...notificationRecieveConfig }, - quote: { optional: true, ...notificationRecieveConfig }, - reaction: { optional: true, ...notificationRecieveConfig }, - pollEnded: { optional: true, ...notificationRecieveConfig }, - receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, - followRequestAccepted: { optional: true, ...notificationRecieveConfig }, - groupInvited: { optional: true, ...notificationRecieveConfig }, - roleAssigned: { optional: true, ...notificationRecieveConfig }, - achievementEarned: { optional: true, ...notificationRecieveConfig }, - app: { optional: true, ...notificationRecieveConfig }, - test: { optional: true, ...notificationRecieveConfig }, + app: notificationRecieveConfig, + quote: notificationRecieveConfig, + reply: notificationRecieveConfig, + follow: notificationRecieveConfig, + renote: notificationRecieveConfig, + mention: notificationRecieveConfig, + reaction: notificationRecieveConfig, + pollEnded: notificationRecieveConfig, + receiveFollowRequest: notificationRecieveConfig, + groupInvited: notificationRecieveConfig, }, }, emailNotificationTypes: { @@ -646,7 +603,104 @@ export const packedMeDetailedOnlySchema = { policies: { type: 'object', nullable: false, optional: false, - ref: 'RolePolicies', + properties: { + gtlAvailable: { + type: 'boolean', + nullable: false, optional: false, + }, + ltlAvailable: { + type: 'boolean', + nullable: false, optional: false, + }, + canPublicNote: { + type: 'boolean', + nullable: false, optional: false, + }, + canInvite: { + type: 'boolean', + nullable: false, optional: false, + }, + inviteLimit: { + type: 'number', + nullable: false, optional: false, + }, + inviteLimitCycle: { + type: 'number', + nullable: false, optional: false, + }, + inviteExpirationTime: { + type: 'number', + nullable: false, optional: false, + }, + canManageCustomEmojis: { + type: 'boolean', + nullable: false, optional: false, + }, + canManageAvatarDecorations: { + type: 'boolean', + nullable: false, optional: false, + }, + canSearchNotes: { + type: 'boolean', + nullable: false, optional: false, + }, + canUseTranslator: { + type: 'boolean', + nullable: false, optional: false, + }, + canHideAds: { + type: 'boolean', + nullable: false, optional: false, + }, + driveCapacityMb: { + type: 'number', + nullable: false, optional: false, + }, + alwaysMarkNsfw: { + type: 'boolean', + nullable: false, optional: false, + }, + pinLimit: { + type: 'number', + nullable: false, optional: false, + }, + antennaLimit: { + type: 'number', + nullable: false, optional: false, + }, + wordMuteLimit: { + type: 'number', + nullable: false, optional: false, + }, + webhookLimit: { + type: 'number', + nullable: false, optional: false, + }, + clipLimit: { + type: 'number', + nullable: false, optional: false, + }, + noteEachClipsLimit: { + type: 'number', + nullable: false, optional: false, + }, + userListLimit: { + type: 'number', + nullable: false, optional: false, + }, + userEachUserListsLimit: { + type: 'number', + nullable: false, optional: false, + }, + rateLimitFactor: { + type: 'number', + nullable: false, optional: false, + }, + avatarDecorationLimit: { + type: 'number', + nullable: false, optional: false, + }, + }, }, //#region secrets email: { @@ -741,5 +795,13 @@ export const packedUserSchema = { type: 'object', ref: 'UserDetailed', }, + { + type: 'object', + ref: 'UserDetailedNotMe', + }, + { + type: 'object', + ref: 'MeDetailed', + }, ], } as const; diff --git a/packages/backend/src/models/util/id.ts b/packages/backend/src/models/util/id.ts index 2d742702c7..c8976aff52 100644 --- a/packages/backend/src/models/util/id.ts +++ b/packages/backend/src/models/util/id.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index e816a7ef71..3d62224485 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -1,17 +1,18 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ // https://github.com/typeorm/typeorm/issues/2400 import pg from 'pg'; +pg.types.setTypeParser(20, Number); + import { DataSource, Logger } from 'typeorm'; import * as highlight from 'cli-highlight'; import { entities as charts } from '@/core/chart/entities.js'; import { MiAbuseReportResolver } from '@/models/AbuseReportResolver.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; -import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAccessToken } from '@/models/AccessToken.js'; import { MiAd } from '@/models/Ad.js'; import { MiAnnouncement } from '@/models/Announcement.js'; @@ -74,7 +75,6 @@ import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; import { MiWebhook } from '@/models/Webhook.js'; -import { MiSystemWebhook } from '@/models/SystemWebhook.js'; import { MiChannel } from '@/models/Channel.js'; import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; import { MiRole } from '@/models/Role.js'; @@ -82,17 +82,14 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js'; import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; -import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -pg.types.setTypeParser(20, Number); - export const dbLogger = new MisskeyLogger('db'); -const sqlLogger = dbLogger.createSubLogger('sql', 'gray'); +const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); class MyCustomLogger implements Logger { @bindThis @@ -179,7 +176,6 @@ export const entities = [ MiHashtag, MiSwSubscription, MiAbuseUserReport, - MiAbuseReportNotificationRecipient, MiRegistrationTicket, MiMessagingMessage, MiSignin, @@ -199,7 +195,6 @@ export const entities = [ MiPasswordResetRequest, MiUserPending, MiWebhook, - MiSystemWebhook, MiUserIp, MiRetentionAggregation, MiRole, @@ -207,7 +202,6 @@ export const entities = [ MiFlash, MiFlashLike, MiUserMemo, - MiBubbleGameRecord, ...charts, ]; @@ -225,24 +219,22 @@ export function createPostgresDataSource(config: Config) { statement_timeout: 1000 * 10, ...config.db.extra, }, - ...(config.dbReplications ? { - replication: { - master: { - host: config.db.host, - port: config.db.port, - username: config.db.user, - password: config.db.pass, - database: config.db.db, - }, - slaves: config.dbSlaves!.map(rep => ({ - host: rep.host, - port: rep.port, - username: rep.user, - password: rep.pass, - database: rep.db, - })), + replication: config.dbReplications ? { + master: { + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, }, - } : {}), + slaves: config.dbSlaves!.map(rep => ({ + host: rep.host, + port: rep.port, + username: rep.user, + password: rep.pass, + database: rep.db, + })), + } : undefined, synchronize: process.env.NODE_ENV === 'test', dropSchema: process.env.NODE_ENV === 'test', cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbをcloseしても何故かredisのコネクションが内部的に残り続けるようで、テストの際に支障が出るため無効にする(キャッシュも含めてテストしたいため本当は有効にしたいが...) diff --git a/packages/backend/src/queue/QueueLoggerService.ts b/packages/backend/src/queue/QueueLoggerService.ts index 65869afd46..a7f2cdd407 100644 --- a/packages/backend/src/queue/QueueLoggerService.ts +++ b/packages/backend/src/queue/QueueLoggerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 4a77d7d8c4..aae9f438f2 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -11,8 +11,7 @@ import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; -import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; -import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; +import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; @@ -25,7 +24,6 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; -import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; @@ -40,7 +38,6 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; -import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js'; @Module({ imports: [ @@ -57,7 +54,6 @@ import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteD DeleteDriveFilesProcessorService, ExportCustomEmojisProcessorService, ExportNotesProcessorService, - ExportClipsProcessorService, ExportFavoritesProcessorService, ExportFollowingProcessorService, ExportMutingProcessorService, @@ -75,10 +71,8 @@ import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteD CleanRemoteFilesProcessorService, RelationshipProcessorService, ReportAbuseProcessorService, - UserWebhookDeliverProcessorService, - SystemWebhookDeliverProcessorService, + WebhookDeliverProcessorService, EndedPollNotificationProcessorService, - ScheduledNoteDeleteProcessorService, DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 901f9ea2da..b6d95c1893 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -1,25 +1,22 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; import * as Redis from 'ioredis'; -import * as Sentry from '@sentry/node'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; -import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; +import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; -import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js'; import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; @@ -45,7 +42,6 @@ import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QUEUE, baseQueueOptions } from './const.js'; -import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js'; // ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 function httpRelatedBackoff(attemptsMade: number) { @@ -80,12 +76,10 @@ export class QueueProcessorService implements OnApplicationShutdown { private dbQueueWorker: Bull.Worker; private deliverQueueWorker: Bull.Worker; private inboxQueueWorker: Bull.Worker; - private userWebhookDeliverQueueWorker: Bull.Worker; - private systemWebhookDeliverQueueWorker: Bull.Worker; + private webhookDeliverQueueWorker: Bull.Worker; private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; - private scheduledNoteDeleteQueueWorker: Bull.Worker; constructor( @Inject(DI.config) @@ -95,16 +89,13 @@ export class QueueProcessorService implements OnApplicationShutdown { private redisForJobQueue: Redis.Redis, private queueLoggerService: QueueLoggerService, - private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, - private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, + private webhookDeliverProcessorService: WebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, - private scheduledNoteDeleteProcessorservice: ScheduledNoteDeleteProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, private exportNotesProcessorService: ExportNotesProcessorService, - private exportClipsProcessorService: ExportClipsProcessorService, private exportFavoritesProcessorService: ExportFavoritesProcessorService, private exportFollowingProcessorService: ExportFollowingProcessorService, private exportMutingProcessorService: ExportMutingProcessorService, @@ -148,383 +139,199 @@ export class QueueProcessorService implements OnApplicationShutdown { } //#region system - { - const processer = (job: Bull.Job) => { - switch (job.name) { - case 'tickCharts': return this.tickChartsProcessorService.process(); - case 'resyncCharts': return this.resyncChartsProcessorService.process(); - case 'cleanCharts': return this.cleanChartsProcessorService.process(); - case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); - case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); - case 'clean': return this.cleanProcessorService.process(); - default: throw new Error(`unrecognized job type ${job.name} for system`); - } - }; - - this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.SYSTEM, this.redisForJobQueue), - autorun: false, - }); - - const logger = this.logger.createSubLogger('system'); - - this.systemQueueWorker - .on('active', (job) => logger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err: Error) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { + switch (job.name) { + case 'tickCharts': return this.tickChartsProcessorService.process(); + case 'resyncCharts': return this.resyncChartsProcessorService.process(); + case 'cleanCharts': return this.cleanChartsProcessorService.process(); + case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); + case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); + case 'clean': return this.cleanProcessorService.process(); + default: throw new Error(`unrecognized job type ${job.name} for system`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.SYSTEM, this.redisForJobQueue), + autorun: false, + }); + + const systemLogger = this.logger.createSubLogger('system'); + + this.systemQueueWorker + .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); //#endregion //#region db - { - const processer = (job: Bull.Job) => { - switch (job.name) { - case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); - case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); - case 'exportNotes': return this.exportNotesProcessorService.process(job); - case 'exportClips': return this.exportClipsProcessorService.process(job); - case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); - case 'exportFollowing': return this.exportFollowingProcessorService.process(job); - case 'exportMuting': return this.exportMutingProcessorService.process(job); - case 'exportBlocking': return this.exportBlockingProcessorService.process(job); - case 'exportUserLists': return this.exportUserListsProcessorService.process(job); - case 'exportAntennas': return this.exportAntennasProcessorService.process(job); - case 'importFollowing': return this.importFollowingProcessorService.process(job); - case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); - case 'importMuting': return this.importMutingProcessorService.process(job); - case 'importBlocking': return this.importBlockingProcessorService.process(job); - case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); - case 'importUserLists': return this.importUserListsProcessorService.process(job); - case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); - case 'importAntennas': return this.importAntennasProcessorService.process(job); - case 'deleteAccount': return this.deleteAccountProcessorService.process(job); - case 'reportAbuse': return this.reportAbuseProcessorService.process(job); - default: throw new Error(`unrecognized job type ${job.name} for db`); - } - }; - - this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.DB, this.redisForJobQueue), - autorun: false, - }); - - const logger = this.logger.createSubLogger('db'); - - this.dbQueueWorker - .on('active', (job) => logger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { + switch (job.name) { + case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); + case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); + case 'exportNotes': return this.exportNotesProcessorService.process(job); + case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); + case 'exportFollowing': return this.exportFollowingProcessorService.process(job); + case 'exportMuting': return this.exportMutingProcessorService.process(job); + case 'exportBlocking': return this.exportBlockingProcessorService.process(job); + case 'exportUserLists': return this.exportUserListsProcessorService.process(job); + case 'exportAntennas': return this.exportAntennasProcessorService.process(job); + case 'importFollowing': return this.importFollowingProcessorService.process(job); + case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); + case 'importMuting': return this.importMutingProcessorService.process(job); + case 'importBlocking': return this.importBlockingProcessorService.process(job); + case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); + case 'importUserLists': return this.importUserListsProcessorService.process(job); + case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); + case 'importAntennas': return this.importAntennasProcessorService.process(job); + case 'deleteAccount': return this.deleteAccountProcessorService.process(job); + case 'reportAbuse': return this.reportAbuseProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for db`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.DB, this.redisForJobQueue), + autorun: false, + }); + + const dbLogger = this.logger.createSubLogger('db'); + + this.dbQueueWorker + .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); //#endregion //#region deliver - { - this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job)); - } else { - return this.deliverProcessorService.process(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.DELIVER, this.redisForJobQueue), - autorun: false, - concurrency: this.config.deliverJobConcurrency ?? 128, - limiter: { - max: this.config.deliverJobPerSec ?? 128, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); - - const logger = this.logger.createSubLogger('deliver'); - - this.deliverQueueWorker - .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Deliver: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.DELIVER, this.redisForJobQueue), + autorun: false, + concurrency: this.config.deliverJobConcurrency ?? 128, + limiter: { + max: this.config.deliverJobPerSec ?? 128, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const deliverLogger = this.logger.createSubLogger('deliver'); + + this.deliverQueueWorker + .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); //#endregion //#region inbox - { - this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job)); - } else { - return this.inboxProcessorService.process(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.INBOX, this.redisForJobQueue), - autorun: false, - concurrency: this.config.inboxJobConcurrency ?? 16, - limiter: { - max: this.config.inboxJobPerSec ?? 32, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); - - const logger = this.logger.createSubLogger('inbox'); - - this.inboxQueueWorker - .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Inbox: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } - //#endregion - - //#region user-webhook deliver - { - this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job)); - } else { - return this.userWebhookDeliverProcessorService.process(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER, this.redisForJobQueue), - autorun: false, - concurrency: 64, - limiter: { - max: 64, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); - - const logger = this.logger.createSubLogger('user-webhook'); - - this.userWebhookDeliverQueueWorker - .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.INBOX, this.redisForJobQueue), + autorun: false, + concurrency: this.config.inboxJobConcurrency ?? 16, + limiter: { + max: this.config.inboxJobPerSec ?? 32, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const inboxLogger = this.logger.createSubLogger('inbox'); + + this.inboxQueueWorker + .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) + .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); //#endregion - //#region system-webhook deliver - { - this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job)); - } else { - return this.systemWebhookDeliverProcessorService.process(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER, this.redisForJobQueue), - autorun: false, - concurrency: 16, - limiter: { - max: 16, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); - - const logger = this.logger.createSubLogger('system-webhook'); - - this.systemWebhookDeliverQueueWorker - .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + //#region webhook deliver + this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER, this.redisForJobQueue), + autorun: false, + concurrency: 64, + limiter: { + max: 64, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const webhookLogger = this.logger.createSubLogger('webhook'); + + this.webhookDeliverQueueWorker + .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); //#endregion //#region relationship - { - const processer = (job: Bull.Job) => { - switch (job.name) { - case 'follow': return this.relationshipProcessorService.processFollow(job); - case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); - case 'block': return this.relationshipProcessorService.processBlock(job); - case 'unblock': return this.relationshipProcessorService.processUnblock(job); - default: throw new Error(`unrecognized job type ${job.name} for relationship`); - } - }; - - this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP, this.redisForJobQueue), - autorun: false, - concurrency: this.config.relationshipJobConcurrency ?? 16, - limiter: { - max: this.config.relationshipJobPerSec ?? 64, - duration: 1000, - }, - }); - - const logger = this.logger.createSubLogger('relationship'); - - this.relationshipQueueWorker - .on('active', (job) => logger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { + switch (job.name) { + case 'follow': return this.relationshipProcessorService.processFollow(job); + case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); + case 'block': return this.relationshipProcessorService.processBlock(job); + case 'unblock': return this.relationshipProcessorService.processUnblock(job); + default: throw new Error(`unrecognized job type ${job.name} for relationship`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP, this.redisForJobQueue), + autorun: false, + concurrency: this.config.relashionshipJobConcurrency ?? 16, + limiter: { + max: this.config.relashionshipJobPerSec ?? 64, + duration: 1000, + }, + }); + + const relationshipLogger = this.logger.createSubLogger('relationship'); + + this.relationshipQueueWorker + .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); //#endregion //#region object storage - { - const processer = (job: Bull.Job) => { - switch (job.name) { - case 'deleteFile': return this.deleteFileProcessorService.process(job); - case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); - default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); - } - }; - - this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE, this.redisForJobQueue), - autorun: false, - concurrency: 16, - }); - - const logger = this.logger.createSubLogger('objectStorage'); - - this.objectStorageQueueWorker - .on('active', (job) => logger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); - if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } - }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); - } + this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { + switch (job.name) { + case 'deleteFile': return this.deleteFileProcessorService.process(job); + case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE, this.redisForJobQueue), + autorun: false, + concurrency: 16, + }); + + const objectStorageLogger = this.logger.createSubLogger('objectStorage'); + + this.objectStorageQueueWorker + .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); //#endregion //#region ended poll notification - { - this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job)); - } else { - return this.endedPollNotificationProcessorService.process(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION, this.redisForJobQueue), - autorun: false, - }); - } - //#endregion - - //#region scheduled note delete - { - this.scheduledNoteDeleteQueueWorker = new Bull.Worker(QUEUE.SCHEDULED_NOTE_DELETE, (job) => { - if (this.config.sentryForBackend) { - return Sentry.startSpan({ name: 'Queue: ScheduledNoteDelete' }, () => this.scheduledNoteDeleteProcessorservice.process(job)); - } else { - return this.scheduledNoteDeleteProcessorservice.process(job); - } - }, { - ...baseQueueOptions(this.config, QUEUE.SCHEDULED_NOTE_DELETE, this.redisForJobQueue), - autorun: false, - }); - } + this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION, this.redisForJobQueue), + autorun: false, + }); //#endregion } @@ -535,12 +342,10 @@ export class QueueProcessorService implements OnApplicationShutdown { this.dbQueueWorker.run(), this.deliverQueueWorker.run(), this.inboxQueueWorker.run(), - this.userWebhookDeliverQueueWorker.run(), - this.systemWebhookDeliverQueueWorker.run(), + this.webhookDeliverQueueWorker.run(), this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), - this.scheduledNoteDeleteQueueWorker.run(), ]); } @@ -551,12 +356,10 @@ export class QueueProcessorService implements OnApplicationShutdown { this.dbQueueWorker.close(), this.deliverQueueWorker.close(), this.inboxQueueWorker.close(), - this.userWebhookDeliverQueueWorker.close(), - this.systemWebhookDeliverQueueWorker.close(), + this.webhookDeliverQueueWorker.close(), this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), - this.scheduledNoteDeleteQueueWorker.close(), ]); } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 81d80cbc83..6ebaaac047 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -12,12 +12,10 @@ export const QUEUE = { INBOX: 'inbox', SYSTEM: 'system', ENDED_POLL_NOTIFICATION: 'endedPollNotification', - SCHEDULED_NOTE_DELETE: 'scheduledNoteDelete', DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', - USER_WEBHOOK_DELIVER: 'userWebhookDeliver', - SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver', + WEBHOOK_DELIVER: 'webhookDeliver', }; export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE], redisConnection: Redis.Redis): Bull.QueueOptions { diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts index 4769cccabf..ca0ddcef95 100644 --- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts +++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index 448fc9c763..5d722b4369 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 110468801c..ae0051363b 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index ec648c9a31..0ffe160d52 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index 1c1739a7ba..22e5fb7c1b 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -63,7 +63,7 @@ export class CleanRemoteFilesProcessorService { isLink: false, }); - job.updateProgress(100 / total * deletedCount); + job.updateProgress(deletedCount / total); } this.logger.succ('All cached remote files has been deleted.'); diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 470ea91cdb..404852787f 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -77,6 +77,10 @@ export class DeleteAccountProcessorService { cursor = notes.at(-1)?.id ?? null; await this.notesRepository.delete(notes.map(note => note.id)); + + for (const note of notes) { + await this.searchService.unindexNote(note); + } } this.logger.succ(`All of notes deleted: ${job.data.user.id}`); diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 61e64c18f6..2f2f3192d9 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts index 9d404d649e..cd9266eb7d 100644 --- a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index d665945861..5e34db845e 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -1,11 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import * as Bull from 'bullmq'; -import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { InstancesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; @@ -63,7 +62,7 @@ export class DeliverProcessorService { if (suspendedHosts == null) { suspendedHosts = await this.instancesRepository.find({ where: { - suspensionState: Not('none'), + isSuspended: true, }, }); this.suspendedHostsCache.set(suspendedHosts); @@ -73,14 +72,13 @@ export class DeliverProcessorService { } try { - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content); // Update stats this.federatedInstanceService.fetch(host).then(i => { if (i.isNotResponding) { this.federatedInstanceService.update(i.id, { isNotResponding: false, - notRespondingSince: null, }); } @@ -100,20 +98,6 @@ export class DeliverProcessorService { if (!i.isNotResponding) { this.federatedInstanceService.update(i.id, { isNotResponding: true, - notRespondingSince: new Date(), - }); - } else if (i.notRespondingSince) { - // 1週間以上不通ならサスペンド - if (i.suspensionState === 'none' && i.notRespondingSince.getTime() <= Date.now() - 1000 * 60 * 60 * 24 * 7) { - this.federatedInstanceService.update(i.id, { - suspensionState: 'autoSuspendedForNotResponding', - }); - } - } else { - // isNotRespondingがtrueでnotRespondingSinceがnullの場合はnotRespondingSinceをセット - // notRespondingSinceは新たな機能なので、それ以前のデータにはnotRespondingSinceがない場合がある - this.federatedInstanceService.update(i.id, { - notRespondingSince: new Date(), }); } @@ -127,12 +111,12 @@ export class DeliverProcessorService { if (res instanceof StatusError) { // 4xx - if (!res.isRetryable) { + if (res.isClientError) { // 相手が閉鎖していることを明示しているため、配送停止する if (job.data.isSharedInbox && res.statusCode === 410) { this.federatedInstanceService.fetch(host).then(i => { this.federatedInstanceService.update(i.id, { - suspensionState: 'goneSuspended', + isSuspended: true, }); }); throw new Bull.UnrecoverableError(`${host} is gone`); diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 34180e5f2b..83b9ff3228 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { PollVotesRepository, NotesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; -import { CacheService } from '@/core/CacheService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; @@ -25,7 +24,6 @@ export class EndedPollNotificationProcessorService { @Inject(DI.pollVotesRepository) private pollVotesRepository: PollVotesRepository, - private cacheService: CacheService, private notificationService: NotificationService, private queueLoggerService: QueueLoggerService, ) { @@ -49,12 +47,9 @@ export class EndedPollNotificationProcessorService { const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; for (const userId of userIds) { - const profile = await this.cacheService.userProfileCache.fetch(userId); - if (profile.userHost === null) { - this.notificationService.createNotification(userId, 'pollEnded', { - noteId: note.id, - }); - } + this.notificationService.createNotification(userId, 'pollEnded', { + noteId: note.id, + }); } } } diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index 88c4ea29c0..d4645078bc 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -81,9 +81,9 @@ export class ExportAntennasProcessorService { }) : null, caseSensitive: antenna.caseSensitive, localOnly: antenna.localOnly, - excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, + notify: antenna.notify, })); if (antennas.length - 1 !== index) { write(', '); diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index 6ec3c18786..c0684e879e 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts deleted file mode 100644 index f463c36204..0000000000 --- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts +++ /dev/null @@ -1,204 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as fs from 'node:fs'; -import { Writable } from 'node:stream'; -import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; -import { format as dateFormat } from 'date-fns'; -import { DI } from '@/di-symbols.js'; -import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, PollsRepository, UsersRepository } from '@/models/_.js'; -import type Logger from '@/logger.js'; -import { DriveService } from '@/core/DriveService.js'; -import { createTemp } from '@/misc/create-temp.js'; -import type { MiPoll } from '@/models/Poll.js'; -import type { MiNote } from '@/models/Note.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type * as Bull from 'bullmq'; -import type { DbJobDataWithUser } from '../types.js'; - -@Injectable() -export class ExportClipsProcessorService { - private logger: Logger; - - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.pollsRepository) - private pollsRepository: PollsRepository, - - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - - private driveService: DriveService, - private queueLoggerService: QueueLoggerService, - private idService: IdService, - ) { - this.logger = this.queueLoggerService.logger.createSubLogger('export-clips'); - } - - @bindThis - public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting clips of ${job.data.user.id} ...`); - - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); - if (user == null) { - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - this.logger.info(`Temp file is ${path}`); - - try { - const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' })); - const writer = stream.getWriter(); - writer.closed.catch(this.logger.error); - - await writer.write('['); - - await this.processClips(writer, user, job); - - await writer.write(']'); - await writer.close(); - - this.logger.succ(`Exported to: ${path}`); - - const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; - const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); - - this.logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - } - - async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job) { - let exportedClipsCount = 0; - let cursor: MiClip['id'] | null = null; - - while (true) { - const clips = await this.clipsRepository.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); - - if (clips.length === 0) { - job.updateProgress(100); - break; - } - - cursor = clips.at(-1)?.id ?? null; - - for (const clip of clips) { - // Stringify but remove the last `]}` - const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2); - const isFirst = exportedClipsCount === 0; - await writer.write(isFirst ? content : ',\n' + content); - - await this.processClipNotes(writer, clip.id); - - await writer.write(']}'); - exportedClipsCount++; - } - - const total = await this.clipsRepository.countBy({ - userId: user.id, - }); - - job.updateProgress(exportedClipsCount / total); - } - } - - async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise { - let exportedClipNotesCount = 0; - let cursor: MiClipNote['id'] | null = null; - - while (true) { - const clipNotes = await this.clipNotesRepository.find({ - where: { - clipId, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - relations: ['note', 'note.user'], - }) as (MiClipNote & { note: MiNote & { user: MiUser } })[]; - - if (clipNotes.length === 0) { - break; - } - - cursor = clipNotes.at(-1)?.id ?? null; - - for (const clipNote of clipNotes) { - let poll: MiPoll | undefined; - if (clipNote.note.hasPoll) { - poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id }); - } - const content = JSON.stringify(this.serializeClipNote(clipNote, poll)); - const isFirst = exportedClipNotesCount === 0; - await writer.write(isFirst ? content : ',\n' + content); - - exportedClipNotesCount++; - } - } - } - - private serializeClip(clip: MiClip): Record { - return { - id: clip.id, - name: clip.name, - description: clip.description, - lastClippedAt: clip.lastClippedAt?.toISOString(), - clipNotes: [], - }; - } - - private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record { - return { - id: clip.id, - createdAt: this.idService.parse(clip.id).date.toISOString(), - note: { - id: clip.note.id, - text: clip.note.text, - createdAt: this.idService.parse(clip.note.id).date.toISOString(), - fileIds: clip.note.fileIds, - replyId: clip.note.replyId, - renoteId: clip.note.renoteId, - poll: poll, - cw: clip.note.cw, - visibility: clip.note.visibility, - visibleUserIds: clip.note.visibleUserIds, - localOnly: clip.note.localOnly, - reactionAcceptance: clip.note.reactionAcceptance, - uri: clip.note.uri, - url: clip.note.url, - user: { - id: clip.note.user.id, - name: clip.note.user.name, - username: clip.note.user.username, - host: clip.note.user.host, - uri: clip.note.user.uri, - }, - }, - }; - } -} diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index e4eb4791bd..983b55dcc3 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index 7bb626dd31..c255d9f1ec 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 1cc80e66d7..8bb78bc12c 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index 243b74f2c2..36504517cb 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 7a10ea3a50..d49cdfd904 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { ReadableStream, TextEncoderStream } from 'node:stream/web'; +import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; @@ -18,85 +18,10 @@ import { bindThis } from '@/decorators.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { Packed } from '@/misc/json-schema.js'; import { IdService } from '@/core/IdService.js'; -import { JsonArrayStream } from '@/misc/JsonArrayStream.js'; -import { FileWriterStream } from '@/misc/FileWriterStream.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; -class NoteStream extends ReadableStream> { - constructor( - job: Bull.Job, - notesRepository: NotesRepository, - pollsRepository: PollsRepository, - driveFileEntityService: DriveFileEntityService, - idService: IdService, - userId: string, - ) { - let exportedNotesCount = 0; - let cursor: MiNote['id'] | null = null; - - const serialize = ( - note: MiNote, - poll: MiPoll | null, - files: Packed<'DriveFile'>[], - ): Record => { - return { - id: note.id, - text: note.text, - createdAt: idService.parse(note.id).date.toISOString(), - updatedAt: note.updatedAt?.toISOString(), - updatedAtHistory: note.updatedAtHistory?.map(x => x.toISOString()), - noteEditHistory: note.noteEditHistory, - fileIds: note.fileIds, - files: files, - replyId: note.replyId, - renoteId: note.renoteId, - poll: poll, - cw: note.cw, - visibility: note.visibility, - visibleUserIds: note.visibleUserIds, - localOnly: note.localOnly, - reactionAcceptance: note.reactionAcceptance, - }; - }; - - super({ - async pull(controller): Promise { - const notes = await notesRepository.find({ - where: { - userId, - ...(cursor !== null ? { id: MoreThan(cursor) } : {}), - }, - take: 100, // 100件ずつ取得 - order: { id: 1 }, - }); - - if (notes.length === 0) { - job.updateProgress(100); - controller.close(); - } - - cursor = notes.at(-1)?.id ?? null; - - for (const note of notes) { - const poll = note.hasPoll - ? await pollsRepository.findOneByOrFail({ noteId: note.id }) // N+1 - : null; - const files = await driveFileEntityService.packManyByIds(note.fileIds); // N+1 - const content = serialize(note, poll, files); - - controller.enqueue(content); - exportedNotesCount++; - } - - const total = await notesRepository.countBy({ userId }); - job.updateProgress(exportedNotesCount / total); - }, - }); - } -} - @Injectable() export class ExportNotesProcessorService { private logger: Logger; @@ -134,19 +59,67 @@ export class ExportNotesProcessorService { this.logger.info(`Temp file is ${path}`); try { - // メモリが足りなくならないようにストリームで処理する - await new NoteStream( - job, - this.notesRepository, - this.pollsRepository, - this.driveFileEntityService, - this.idService, - user.id, - ) - .pipeThrough(new JsonArrayStream()) - .pipeThrough(new TextEncoderStream()) - .pipeTo(new FileWriterStream(path)); + const stream = fs.createWriteStream(path, { flags: 'a' }); + + const write = (text: string): Promise => { + return new Promise((res, rej) => { + stream.write(text, err => { + if (err) { + this.logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + }; + + await write('['); + + let exportedNotesCount = 0; + let cursor: MiNote['id'] | null = null; + + while (true) { + const notes = await this.notesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as MiNote[]; + + if (notes.length === 0) { + job.updateProgress(100); + break; + } + + cursor = notes.at(-1)?.id ?? null; + for (const note of notes) { + let poll: MiPoll | undefined; + if (note.hasPoll) { + poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); + } + const files = await this.driveFileEntityService.packManyByIds(note.fileIds); + const content = JSON.stringify(this.serialize(note, poll, files)); + const isFirst = exportedNotesCount === 0; + await write(isFirst ? content : ',\n' + content); + exportedNotesCount++; + } + + const total = await this.notesRepository.countBy({ + userId: user.id, + }); + + job.updateProgress(exportedNotesCount / total); + } + + await write(']'); + + stream.end(); this.logger.succ(`Exported to: ${path}`); const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; @@ -157,4 +130,22 @@ export class ExportNotesProcessorService { cleanup(); } } + + private serialize(note: MiNote, poll: MiPoll | null = null, files: Packed<'DriveFile'>[]): Record { + return { + id: note.id, + text: note.text, + createdAt: this.idService.parse(note.id).date.toISOString(), + fileIds: note.fileIds, + files: files, + replyId: note.replyId, + renoteId: note.renoteId, + poll: poll, + cw: note.cw, + visibility: note.visibility, + visibleUserIds: note.visibleUserIds, + localOnly: note.localOnly, + reactionAcceptance: note.reactionAcceptance, + }; + } } diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index ee87cff5d3..4af6720a5d 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index 9c033b73e2..e2ee6a4712 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -44,11 +44,11 @@ const validate = new Ajv().compile({ } }, caseSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, - excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, + notify: { type: 'boolean' }, }, - required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], + required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], }); @Injectable() @@ -76,7 +76,7 @@ export class ImportAntennasProcessorService { this.logger.warn('Validation Failed'); continue; } - const result = await this.antennasRepository.insertOne({ + const result = await this.antennasRepository.insert({ id: this.idService.gen(now.getTime()), lastUsedAt: now, userId: job.data.user.id, @@ -88,10 +88,10 @@ export class ImportAntennasProcessorService { users: (antenna.src === 'list' && antenna.userListAccts !== null ? antenna.userListAccts : antenna.users).filter(Boolean), caseSensitive: antenna.caseSensitive, localOnly: antenna.localOnly, - excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, - }); + notify: antenna.notify, + }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); this.logger.succ('Antenna created: ' + result.id); this.globalEventService.publishInternalEvent('antennaCreated', result); } diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index b78229c648..360b1a4332 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 171809d25c..18c70978aa 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index 70c9f3a096..9e341988f6 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index ec9d2b6c4c..f0ed524e49 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index db9255b35d..9f103c106f 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -79,11 +79,11 @@ export class ImportUserListsProcessorService { }); if (list == null) { - list = await this.userListsRepository.insertOne({ + list = await this.userListsRepository.insert({ id: this.idService.gen(), userId: user.id, name: listName, - }); + }).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); } let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index fa7009f8f5..4e28475d15 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,17 +15,15 @@ import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import FederationChart from '@/core/chart/charts/federation.js'; import { getApId } from '@/core/activitypub/type.js'; -import type { IActivity } from '@/core/activitypub/type.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; +import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { InboxJobData } from '../types.js'; @@ -39,7 +37,7 @@ export class InboxProcessorService { private apInboxService: ApInboxService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, - private jsonLdService: JsonLdService, + private ldSignatureService: LdSignatureService, private apPersonService: ApPersonService, private apDbResolverService: ApDbResolverService, private instanceChart: InstanceChart, @@ -53,7 +51,7 @@ export class InboxProcessorService { @bindThis public async process(job: Bull.Job): Promise { const signature = job.data.signature; // HTTP-signature - let activity = job.data.activity; + const activity = job.data.activity; //#region Log const info = Object.assign({}, activity); @@ -87,7 +85,7 @@ export class InboxProcessorService { } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { - if (!err.isRetryable) { + if (err.isClientError) { throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`); } throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`); @@ -111,21 +109,20 @@ export class InboxProcessorService { // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { // 一致しなくても、でもLD-Signatureがありそうならそっちも見る - const ldSignature = activity.signature; - if (ldSignature) { - if (ldSignature.type !== 'RsaSignature2017') { - throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`); + if (activity.signature) { + if (activity.signature.type !== 'RsaSignature2017') { + throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); } - // ldSignature.creator: https://example.oom/users/user#main-key + // activity.signature.creator: https://example.oom/users/user#main-key // みたいになっててUserを引っ張れば公開キーも入ることを期待する - if (ldSignature.creator) { - const candicate = ldSignature.creator.replace(/#.*/, ''); + if (activity.signature.creator) { + const candicate = activity.signature.creator.replace(/#.*/, ''); await this.apPersonService.resolvePerson(candicate).catch(() => null); } // keyIdからLD-Signatureのユーザーを取得 - authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator); + authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator); if (authUser == null) { throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした'); } @@ -134,31 +131,13 @@ export class InboxProcessorService { throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'); } - const jsonLd = this.jsonLdService.use(); - // LD-Signature検証 - const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); + const ldSignature = this.ldSignatureService.use(); + const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); if (!verified) { throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); } - // アクティビティを正規化 - delete activity.signature; - try { - activity = await jsonLd.compact(activity) as IActivity; - } catch (e) { - throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`); - } - // TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする - // https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29 - activity.signature = ldSignature; - - //#region Log - const compactedInfo = Object.assign({}, activity); - delete compactedInfo['@context']; - this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`); - //#endregion - // もう一度actorチェック if (authUser.user.uri !== activity.actor) { throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`); @@ -188,8 +167,6 @@ export class InboxProcessorService { this.federatedInstanceService.update(i.id, { latestRequestReceivedAt: new Date(), isNotResponding: false, - // もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる - suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined, }); this.fetchInstanceMetadataService.fetchInstanceMetadata(i); @@ -203,26 +180,7 @@ export class InboxProcessorService { }); // アクティビティを処理 - try { - const result = await this.apInboxService.performActivity(authUser.user, activity); - if (result && !result.startsWith('ok')) { - this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`); - return result; - } - } catch (e) { - if (e instanceof IdentifiableError) { - if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { - return 'blocked notes with prohibited words'; - } - if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') { - return 'actor has been suspended'; - } - if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note - return e.message; - } - } - throw e; - } + await this.apInboxService.performActivity(authUser.user, activity); return 'ok'; } } diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts index 99f4897a1c..c29b1ab914 100644 --- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts +++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts b/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts index 5392f78639..f005fc7839 100644 --- a/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts +++ b/packages/backend/src/queue/processors/ReportAbuseProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index 570cdf9a75..f6a6dcb3ad 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/ScheduledNoteDeleteProcessorService.ts b/packages/backend/src/queue/processors/ScheduledNoteDeleteProcessorService.ts deleted file mode 100644 index c8578b0fc8..0000000000 --- a/packages/backend/src/queue/processors/ScheduledNoteDeleteProcessorService.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository } from '@/models/_.js'; -import type Logger from '@/logger.js'; -import { bindThis } from '@/decorators.js'; -import { NoteDeleteService } from '@/core/NoteDeleteService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type * as Bull from 'bullmq'; -import type { ScheduledNoteDeleteJobData } from '../types.js'; - -@Injectable() -export class ScheduledNoteDeleteProcessorService { - private logger: Logger; - - constructor( - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private noteDeleteService: NoteDeleteService, - private queueLoggerService: QueueLoggerService, - ) { - this.logger = this.queueLoggerService.logger.createSubLogger('scheduled-note-delete'); - } - - @bindThis - public async process(job: Bull.Job): Promise { - const note = await this.notesRepository.findOneBy({ id: job.data.noteId}); - - if (note == null) { - return; - } - - const user = await this.usersRepository.findOneBy({ id: note.userId }); - - if (user == null) { - return; - } - - await this.noteDeleteService.delete(user, note); - this.logger.info(`Delete note ${note.id}`); - } -} diff --git a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts deleted file mode 100644 index f6bef52684..0000000000 --- a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Bull from 'bullmq'; -import { DI } from '@/di-symbols.js'; -import type { SystemWebhooksRepository } from '@/models/_.js'; -import type { Config } from '@/config.js'; -import type Logger from '@/logger.js'; -import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { StatusError } from '@/misc/status-error.js'; -import { bindThis } from '@/decorators.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import { SystemWebhookDeliverJobData } from '../types.js'; - -@Injectable() -export class SystemWebhookDeliverProcessorService { - private logger: Logger; - - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.systemWebhooksRepository) - private systemWebhooksRepository: SystemWebhooksRepository, - - private httpRequestService: HttpRequestService, - private queueLoggerService: QueueLoggerService, - ) { - this.logger = this.queueLoggerService.logger.createSubLogger('webhook'); - } - - @bindThis - public async process(job: Bull.Job): Promise { - try { - this.logger.debug(`delivering ${job.data.webhookId}`); - - const res = await this.httpRequestService.send(job.data.to, { - method: 'POST', - headers: { - 'User-Agent': 'Misskey-Hooks', - 'X-Misskey-Host': this.config.host, - 'X-Misskey-Hook-Id': job.data.webhookId, - 'X-Misskey-Hook-Secret': job.data.secret, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - server: this.config.url, - hookId: job.data.webhookId, - eventId: job.data.eventId, - createdAt: job.data.createdAt, - type: job.data.type, - body: job.data.content, - }), - }); - - this.systemWebhooksRepository.update({ id: job.data.webhookId }, { - latestSentAt: new Date(), - latestStatus: res.status, - }); - - return 'Success'; - } catch (res) { - this.logger.error(res as Error); - - this.systemWebhooksRepository.update({ id: job.data.webhookId }, { - latestSentAt: new Date(), - latestStatus: res instanceof StatusError ? res.statusCode : 1, - }); - - if (res instanceof StatusError) { - // 4xx - if (!res.isRetryable) { - throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); - } - - // 5xx etc. - throw new Error(`${res.statusCode} ${res.statusMessage}`); - } else { - // DNS error, socket error, timeout ... - throw res; - } - } - } -} diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index 93ec34162d..231af2440f 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts similarity index 88% rename from packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts rename to packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index 9ec630ef70..b3468cd336 100644 --- a/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,10 +13,10 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import { UserWebhookDeliverJobData } from '../types.js'; +import type { WebhookDeliverJobData } from '../types.js'; @Injectable() -export class UserWebhookDeliverProcessorService { +export class WebhookDeliverProcessorService { private logger: Logger; constructor( @@ -33,7 +33,7 @@ export class UserWebhookDeliverProcessorService { } @bindThis - public async process(job: Bull.Job): Promise { + public async process(job: Bull.Job): Promise { try { this.logger.debug(`delivering ${job.data.webhookId}`); @@ -71,7 +71,7 @@ export class UserWebhookDeliverProcessorService { if (res instanceof StatusError) { // 4xx - if (!res.isRetryable) { + if (res.isClientError) { throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index bba9cd021f..e844e65704 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -16,9 +16,7 @@ export type DeliverJobData = { /** Actor */ user: ThinUser; /** Activity */ - content: string; - /** Digest header */ - digest: string; + content: unknown; /** inbox URL to deliver */ to: string; /** whether it is sharedInbox */ @@ -109,21 +107,7 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; -export type ScheduledNoteDeleteJobData = { - noteId: MiNote['id']; -}; - -export type SystemWebhookDeliverJobData = { - type: string; - content: unknown; - webhookId: MiWebhook['id']; - to: string; - secret: string; - createdAt: number; - eventId: string; -}; - -export type UserWebhookDeliverJobData = { +export type WebhookDeliverJobData = { type: string; content: unknown; webhookId: MiWebhook['id']; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 365207b34d..e3db75d42e 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -28,7 +28,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { IActivity } from '@/core/activitypub/type.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { isPureRenote } from '@/misc/is-pure-renote.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FindOptionsWhere } from 'typeorm'; @@ -91,7 +91,7 @@ export class ActivityPubServerService { */ @bindThis private async packActivity(note: MiNote): Promise { - if (isRenote(note) && !isQuote(note)) { + if (isPureRenote(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); } @@ -654,8 +654,6 @@ export class ActivityPubServerService { }); fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { - vary(reply.raw, 'Accept'); - const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -668,8 +666,6 @@ export class ActivityPubServerService { }); fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { - vary(reply.raw, 'Accept'); - const user = await this.usersRepository.findOneBy({ usernameLower: request.params.user.toLowerCase(), host: IsNull(), diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 77a637d895..038c6c7a69 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -9,7 +9,7 @@ import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import rename from 'rename'; import sharp from 'sharp'; -import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; +import { sharpBmp } from 'sharp-read-bmp'; import type { Config } from '@/config.js'; import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -27,7 +27,6 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { correctFilename } from '@/misc/correct-filename.js'; -import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; const _filename = fileURLToPath(import.meta.url); @@ -53,7 +52,7 @@ export class FileServerService { private internalStorageService: InternalStorageService, private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('server', 'gray'); + this.logger = this.loggerService.getLogger('server', 'gray', false); //this.createServer = this.createServer.bind(this); } @@ -68,23 +67,20 @@ export class FileServerService { done(); }); - fastify.register((fastify, options, done) => { - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - fastify.get('/files/app-default.jpg', (request, reply) => { - const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); - reply.header('Content-Type', 'image/jpeg'); - reply.header('Cache-Control', 'max-age=31536000, immutable'); - return reply.send(file); - }); - - fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { - return await this.sendDriveFile(request, reply) - .catch(err => this.errorHandler(request, reply, err)); - }); - fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { - return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`); - }); - done(); + fastify.get('/files/app-default.jpg', (request, reply) => { + const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); + reply.header('Content-Type', 'image/jpeg'); + reply.header('Cache-Control', 'max-age=31536000, immutable'); + return reply.send(file); + }); + + fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { + return await this.sendDriveFile(request, reply) + .catch(err => this.errorHandler(request, reply, err)); + }); + fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { + return await this.sendDriveFile(request, reply) + .catch(err => this.errorHandler(request, reply, err)); }); fastify.get<{ @@ -172,36 +168,11 @@ export class FileServerService { } if (!image) { - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - - image = { - data: fs.createReadStream(file.path, { - start, - end, - }), - ext: file.ext, - type: file.mime, - }; - - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - } else { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; - } + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; } if ('pipe' in image.data && typeof image.data.pipe === 'function') { @@ -214,8 +185,6 @@ export class FileServerService { } reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); - reply.header('Content-Length', file.file.size); - reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition( 'inline', @@ -234,54 +203,11 @@ export class FileServerService { reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream'); reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition('inline', filename)); - - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - const fileStream = fs.createReadStream(file.path, { - start, - end, - }); - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - return fileStream; - } - return fs.createReadStream(file.path); } else { reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream'); - reply.header('Content-Length', file.file.size); reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition('inline', file.filename)); - - if (request.headers.range && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - const fileStream = fs.createReadStream(file.path, { - start, - end, - }); - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - return fileStream; - } - return fs.createReadStream(file.path); } } catch (e) { @@ -414,36 +340,11 @@ export class FileServerService { } if (!image) { - if (request.headers.range && file.file && file.file.size > 0) { - const range = request.headers.range as string; - const parts = range.replace(/bytes=/, '').split('-'); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; - if (end > file.file.size) { - end = file.file.size - 1; - } - const chunksize = end - start + 1; - - image = { - data: fs.createReadStream(file.path, { - start, - end, - }), - ext: file.ext, - type: file.mime, - }; - - reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); - reply.header('Accept-Ranges', 'bytes'); - reply.header('Content-Length', chunksize); - reply.code(206); - } else { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; - } + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; } if ('cleanup' in file) { @@ -533,7 +434,6 @@ export class FileServerService { if (!file.storedInternal) { if (!(file.isLink && file.uri)) return '204'; const result = await this.downloadAndDetectTypeFromUrl(file.uri); - file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので return { ...result, url: file.uri, diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts deleted file mode 100644 index 2c3ed85925..0000000000 --- a/packages/backend/src/server/HealthServerService.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import { DataSource } from 'typeorm'; -import { bindThis } from '@/decorators.js'; -import { DI } from '@/di-symbols.js'; -import { readyRef } from '@/boot/ready.js'; -import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; -import type { MeiliSearch } from 'meilisearch'; - -@Injectable() -export class HealthServerService { - constructor( - @Inject(DI.redis) - private redis: Redis.Redis, - - @Inject(DI.redisForPub) - private redisForPub: Redis.Redis, - - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.meilisearch) - private meilisearch: MeiliSearch | null, - ) {} - - @bindThis - public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - fastify.get('/', async (request, reply) => { - reply.code(await Promise.all([ - new Promise((resolve, reject) => readyRef.value ? resolve() : reject()), - this.redis.ping(), - this.redisForPub.ping(), - this.redisForSub.ping(), - this.redisForTimelines.ping(), - this.db.query('SELECT 1'), - ...(this.meilisearch ? [this.meilisearch.health()] : []), - ]).then(() => 200, () => 503)); - reply.header('Cache-Control', 'no-store'); - }); - - done(); - } -} diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index d993598f5e..d82ba469c5 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -113,19 +113,15 @@ export class NodeinfoServerService { langs: meta.langs, tosUrl: meta.termsOfServiceUrl, privacyPolicyUrl: meta.privacyPolicyUrl, - inquiryUrl: meta.inquiryUrl, impressumUrl: meta.impressumUrl, repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, - statusUrl: meta.statusUrl, disableRegistration: meta.disableRegistration, disableLocalTimeline: !basePolicies.ltlAvailable, disableGlobalTimeline: !basePolicies.gtlAvailable, emailRequiredForSignup: meta.emailRequiredForSignup, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, - enableMcaptcha: meta.enableMcaptcha, - enableTurnstile: meta.enableTurnstile, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 3eb8aef4ed..4f045fb35e 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,7 +8,6 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { ApiCallService } from './api/ApiCallService.js'; import { FileServerService } from './FileServerService.js'; -import { HealthServerService } from './HealthServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ServerService } from './ServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js'; @@ -23,13 +22,9 @@ import { SigninApiService } from './api/SigninApiService.js'; import { SigninService } from './api/SigninService.js'; import { SignupApiService } from './api/SignupApiService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js'; -import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; import { FeedService } from './web/FeedService.js'; import { UrlPreviewService } from './web/UrlPreviewService.js'; -import { ClientLoggerService } from './web/ClientLoggerService.js'; -import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; - import { MainChannelService } from './api/stream/channels/main.js'; import { AdminChannelService } from './api/stream/channels/admin.js'; import { AntennaChannelService } from './api/stream/channels/antenna.js'; @@ -45,7 +40,10 @@ import { MessagingChannelService } from './api/stream/channels/messaging.js'; import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { UserListChannelService } from './api/stream/channels/user-list.js'; +import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; +import { ClientLoggerService } from './web/ClientLoggerService.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; +import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; @Module({ imports: [ @@ -56,7 +54,6 @@ import { RoleTimelineChannelService } from './api/stream/channels/role-timeline. ClientServerService, ClientLoggerService, FeedService, - HealthServerService, UrlPreviewService, ActivityPubServerService, FileServerService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index f7a6764c43..278145b4ab 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,6 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import Fastify, { FastifyInstance } from 'fastify'; import fastifyStatic from '@fastify/static'; import fastifyRawBody from 'fastify-raw-body'; -import rateLimit from '@fastify/rate-limit' import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; @@ -19,6 +18,7 @@ import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { genIdenticon } from '@/misc/gen-identicon.js'; +import { createTemp } from '@/misc/create-temp.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; @@ -29,7 +29,6 @@ import { ApiServerService } from './api/ApiServerService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js'; import { FileServerService } from './FileServerService.js'; -import { HealthServerService } from './HealthServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; @@ -63,13 +62,12 @@ export class ServerService implements OnApplicationShutdown { private wellKnownServerService: WellKnownServerService, private nodeinfoServerService: NodeinfoServerService, private fileServerService: FileServerService, - private healthServerService: HealthServerService, private clientServerService: ClientServerService, private globalEventService: GlobalEventService, private loggerService: LoggerService, private oauth2ProviderService: OAuth2ProviderService, ) { - this.logger = this.loggerService.getLogger('server', 'gray'); + this.logger = this.loggerService.getLogger('server', 'gray', false); } @bindThis @@ -111,7 +109,6 @@ export class ServerService implements OnApplicationShutdown { fastify.register(this.wellKnownServerService.createServer); fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' }); fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' }); - fastify.register(this.healthServerService.createServer, { prefix: '/healthz' }); fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; @@ -123,20 +120,12 @@ export class ServerService implements OnApplicationShutdown { return; } - const emojiPath = path.replace(/\.webp$/i, ''); - const pathChunks = emojiPath.split('@'); - - if (pathChunks.length > 2) { - reply.code(400); - return; - } - - const name = pathChunks.shift(); - const host = pathChunks.pop(); + const name = path.split('@')[0].replace(/\.webp$/i, ''); + const host = path.split('@')[1]?.replace(/\.webp$/i, ''); const emoji = await this.emojisRepository.findOneBy({ // `@.` is the spec of ReactionService.decodeReaction - host: (host === undefined || host === '.') ? IsNull() : host, + host: (host == null || host === '.') ? IsNull() : host, name: name, }); @@ -195,7 +184,9 @@ export class ServerService implements OnApplicationShutdown { reply.header('Cache-Control', 'public, max-age=86400'); if ((await this.metaService.fetch()).enableIdenticonGeneration) { - return await genIdenticon(request.params.x); + const [temp, cleanup] = await createTemp(); + await genIdenticon(request.params.x, fs.createWriteStream(temp)); + return fs.createReadStream(temp).on('close', () => cleanup()); } else { return reply.redirect('/static-assets/avatar.png'); } @@ -213,7 +204,7 @@ export class ServerService implements OnApplicationShutdown { }); this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 8e326da89a..0f7bff5163 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index b5d9968972..e65d1f50f1 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,6 @@ import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; import * as stream from 'node:stream/promises'; import { Inject, Injectable } from '@nestjs/common'; -import * as Sentry from '@sentry/node'; import { DI } from '@/di-symbols.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; @@ -18,7 +17,6 @@ import { MetaService } from '@/core/MetaService.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import type { Config } from '@/config.js'; import type { FlashToken } from '@/misc/flash-token.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; @@ -41,9 +39,6 @@ export class ApiCallService implements OnApplicationShutdown { private userIpHistoriesClearIntervalId: NodeJS.Timeout; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, @@ -74,16 +69,6 @@ export class ApiCallService implements OnApplicationShutdown { reply.header('WWW-Authenticate', `Bearer realm="CherryPick", error="insufficient_scope", error_description="${err.message}"`); } statusCode = statusCode ?? 403; - } else if (err.code === 'RATE_LIMIT_EXCEEDED') { - const info: unknown = err.info; - const unixEpochInSeconds = Date.now(); - if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') { - const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000); - // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく - reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10)); - } else { - this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`); - } } else if (!statusCode) { statusCode = 500; } @@ -104,51 +89,6 @@ export class ApiCallService implements OnApplicationShutdown { } } - #onExecError(ep: IEndpoint, data: any, err: Error, userId?: MiUser['id']): void { - if (err instanceof ApiError || err instanceof AuthenticationError) { - throw err; - } else { - const errId = randomUUID(); - this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { - ep: ep.name, - ps: data, - e: { - message: err.message, - code: err.name, - stack: err.stack, - id: errId, - }, - }); - - if (this.config.sentryForBackend) { - Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, { - level: 'error', - user: { - id: userId, - }, - extra: { - ep: ep.name, - ps: data, - e: { - message: err.message, - code: err.name, - stack: err.stack, - id: errId, - }, - }, - }); - } - - throw new ApiError(null, { - e: { - message: err.message, - code: err.name, - id: errId, - }, - }); - } - } - @bindThis public handleRequest( endpoint: IEndpoint & { exec: any }, @@ -320,17 +260,12 @@ export class ApiCallService implements OnApplicationShutdown { if (factor > 0) { // Rate limit await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { - if ('info' in err) { - // errはLimiter.LimiterInfoであることが期待される - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }, err.info); - } else { - throw new TypeError('information must be a rate-limiter information.'); - } + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }); }); } } @@ -437,15 +372,31 @@ export class ApiCallService implements OnApplicationShutdown { } // API invoking - if (this.config.sentryForBackend) { - return await Sentry.startSpan({ - name: 'API: ' + ep.name, - }, () => ep.exec(data, user, token, flashToken, file, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))); - } else { - return await ep.exec(data, user, token, flashToken, file, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)); - } + return await ep.exec(data, user, token, flashToken, file, request.ip, request.headers).catch((err: Error) => { + if (err instanceof ApiError || err instanceof AuthenticationError) { + throw err; + } else { + const errId = randomUUID(); + this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { + ep: ep.name, + ps: data, + e: { + message: err.message, + code: err.name, + stack: err.stack, + id: errId, + }, + }); + console.error(err, errId); + throw new ApiError(null, { + e: { + message: err.message, + code: err.name, + id: errId, + }, + }); + } + }); } @bindThis diff --git a/packages/backend/src/server/api/ApiLoggerService.ts b/packages/backend/src/server/api/ApiLoggerService.ts index 72b71c0b5c..9fbd123983 100644 --- a/packages/backend/src/server/api/ApiLoggerService.ts +++ b/packages/backend/src/server/api/ApiLoggerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 4a5935f930..5b7a6f9838 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -137,7 +137,7 @@ export class ApiServerService { const instances = await this.instancesRepository.find({ select: ['host'], where: { - suspensionState: 'none', + isSuspended: false, }, }); @@ -157,7 +157,7 @@ export class ApiServerService { return { ok: true, token: token.token, - user: await this.userEntityService.pack(token.userId, null, { schema: 'UserDetailedNotMe' }), + user: await this.userEntityService.pack(token.userId, null, { detail: true }), }; } else { return { diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 4154b3e518..90d6d15b31 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 773431cc00..ae10d06144 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -1,23 +1,18 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 { ServerStatsService } from '@/daemons/ServerStatsService.js'; -import * as ep___admin_abuseReportResolver_create from '@/server/api/endpoints/admin/abuse-report-resolver/create.js'; -import * as ep___admin_abuseReportResolver_update from '@/server/api/endpoints/admin/abuse-report-resolver/update.js'; -import * as ep___admin_abuseReportResolver_delete from '@/server/api/endpoints/admin/abuse-report-resolver/delete.js'; -import * as ep___admin_abuseReportResolver_list from '@/server/api/endpoints/admin/abuse-report-resolver/list.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_abuseReportResolver_create from './endpoints/admin/abuse-report-resolver/create.js'; +import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-report-resolver/update.js'; +import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js'; +import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js'; +import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.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'; @@ -83,8 +78,6 @@ 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_suspendUser from './endpoints/admin/suspend-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; -import * as ep___admin_setUserSensitive from './endpoints/admin/set-user-sensitive.js'; -import * as ep___admin_unsetUserSensitive from './endpoints/admin/unset-user-sensitive.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'; @@ -97,13 +90,7 @@ 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___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'; @@ -229,7 +216,6 @@ 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'; @@ -323,8 +309,6 @@ 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_delete from './endpoints/notifications/delete.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'; @@ -411,8 +395,6 @@ import * as ep___users_translate from './endpoints/users/translate.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___bubbleGame_register from './endpoints/bubble-game/register.js'; -import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -423,11 +405,6 @@ const $admin_abuseReportResolver_update: Provider = { provide: 'ep:admin/abuse-r const $admin_abuseReportResolver_list: Provider = { provide: 'ep:admin/abuse-report-resolver/list', useClass: ep___admin_abuseReportResolver_list.default }; const $admin_abuseReportResolver_delete: Provider = { provide: 'ep:admin/abuse-report-resolver/delete', useClass: ep___admin_abuseReportResolver_delete.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 }; @@ -493,8 +470,6 @@ const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep_ const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default }; const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default }; -const $admin_setUserSensitive: Provider = { provide: 'ep:admin/set-user-sensitive', useClass: ep___admin_setUserSensitive.default }; -const $admin_unsetUserSensitive: Provider = { provide: 'ep:admin/unset-user-sensitive', useClass: ep___admin_unsetUserSensitive.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 }; @@ -507,13 +482,7 @@ const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useCla 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 $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 }; @@ -639,7 +608,6 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: 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 }; @@ -733,8 +701,6 @@ const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep 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 $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; -const $notifications_delete: Provider = { provide: 'ep:notifications/delete', useClass: ep___notifications_delete.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 }; @@ -821,8 +787,6 @@ const $users_translate: Provider = { provide: 'ep:users/translate', useClass: ep 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 $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 }; @Module({ imports: [ @@ -838,11 +802,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $admin_abuseReportResolver_list, $admin_abuseReportResolver_update, $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, @@ -908,8 +867,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $admin_showUsers, $admin_suspendUser, $admin_unsuspendUser, - $admin_setUserSensitive, - $admin_unsetUserSensitive, $admin_updateMeta, $admin_deleteAccount, $admin_updateUserNote, @@ -922,13 +879,7 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $admin_roles_unassign, $admin_roles_updateDefaultPolicies, $admin_roles_users, - $admin_systemWebhook_create, - $admin_systemWebhook_delete, - $admin_systemWebhook_list, - $admin_systemWebhook_show, - $admin_systemWebhook_update, $announcements, - $announcements_show, $antennas_create, $antennas_delete, $antennas_list, @@ -1054,7 +1005,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $i_exportFollowing, $i_exportMute, $i_exportNotes, - $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, @@ -1148,8 +1098,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $notes_unrenote, $notes_userListTimeline, $notifications_create, - $notifications_delete, - $notifications_flush, $notifications_markAllAsRead, $notifications_testNotification, $pagePush, @@ -1236,8 +1184,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $fetchRss, $fetchExternalResources, $retention, - $bubbleGame_register, - $bubbleGame_ranking, ], exports: [ $admin_meta, @@ -1246,11 +1192,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $admin_abuseReportResolver_list, $admin_abuseReportResolver_update, $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, @@ -1316,8 +1257,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $admin_showUsers, $admin_suspendUser, $admin_unsuspendUser, - $admin_setUserSensitive, - $admin_unsetUserSensitive, $admin_updateMeta, $admin_deleteAccount, $admin_updateUserNote, @@ -1330,13 +1269,7 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $admin_roles_unassign, $admin_roles_updateDefaultPolicies, $admin_roles_users, - $admin_systemWebhook_create, - $admin_systemWebhook_delete, - $admin_systemWebhook_list, - $admin_systemWebhook_show, - $admin_systemWebhook_update, $announcements, - $announcements_show, $antennas_create, $antennas_delete, $antennas_list, @@ -1462,7 +1395,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $i_exportFollowing, $i_exportMute, $i_exportNotes, - $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, @@ -1555,10 +1487,7 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $notes_unrenote, $notes_userListTimeline, $notifications_create, - $notifications_delete, - $notifications_flush, $notifications_markAllAsRead, - $notifications_testNotification, $pagePush, $pages_create, $pages_delete, @@ -1641,8 +1570,6 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $fetchRss, $fetchExternalResources, $retention, - $bubbleGame_register, - $bubbleGame_ranking, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/FtsQueryService.ts b/packages/backend/src/server/api/FtsQueryService.ts deleted file mode 100644 index e246db9e21..0000000000 --- a/packages/backend/src/server/api/FtsQueryService.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Inject, Injectable } from "@nestjs/common"; -import { Brackets, IsNull, Not, SelectQueryBuilder } from "typeorm"; -import type { NotesRepository, UsersRepository } from "@/models/_.js"; -import { QueryService } from "@/core/QueryService.js"; -import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; -import { FILE_TYPE_BROWSERSAFE } from "@/const.js"; -import { DI } from "@/di-symbols.js"; -import { bindThis } from "@/decorators.js"; - -const filters = { -} as Record, search: string) => any>; - -@Injectable() -export class FtsQueryService { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private queryService: QueryService, - ) { - - } - - @bindThis - public generateFtsQuery(query: SelectQueryBuilder, q: string): void { - const components = q.split(" "); - const terms: string[] = []; - - for (const component of components) { - const split = component.split(":"); - if (split.length > 1 && filters[split[0]] !== undefined ) { - filters[split[0]](query, split.slice(1).join(":")); - } else { - terms.push(component); - } - } - - for (const term of terms) { - if (term.startsWith('-')) { - query.andWhere("note.text NOT ILIKE :q", { q: `%${ sqlLikeEscape(term.substring(1)) }%` }); - } else { - query.andWhere("note.text ILIKE :q", { q: `%${ sqlLikeEscape(term) }%`}) - } - } - } -} diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 68a1de6026..b504ae54cc 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index 52d73baa0a..cfdd53d1a1 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -32,13 +32,11 @@ export class RateLimiterService { @bindThis public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1) { - { - if (this.disabled) { - return Promise.resolve(); - } + return new Promise((ok, reject) => { + if (this.disabled) ok(); // Short-term limit - const min = new Promise((ok, reject) => { + const min = (): void => { const minIntervalLimiter = new Limiter({ id: `${actor}:${limitation.key}:min`, duration: limitation.minInterval! * factor, @@ -48,25 +46,25 @@ export class RateLimiterService { minIntervalLimiter.get((err, info) => { if (err) { - return reject({ code: 'ERR', info }); + return reject('ERR'); } this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); if (info.remaining === 0) { - return reject({ code: 'BRIEF_REQUEST_INTERVAL', info }); + reject('BRIEF_REQUEST_INTERVAL'); } else { if (hasLongTermLimit) { - return max.then(ok, reject); + max(); } else { - return ok(); + ok(); } } }); - }); + }; // Long term limit - const max = new Promise((ok, reject) => { + const max = (): void => { const limiter = new Limiter({ id: `${actor}:${limitation.key}`, duration: limitation.duration! * factor, @@ -76,18 +74,18 @@ export class RateLimiterService { limiter.get((err, info) => { if (err) { - return reject({ code: 'ERR', info }); + return reject('ERR'); } this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); if (info.remaining === 0) { - return reject({ code: 'RATE_LIMIT_EXCEEDED', info }); + reject('RATE_LIMIT_EXCEEDED'); } else { - return ok(); + ok(); } }); - }); + }; const hasShortTermLimit = typeof limitation.minInterval === 'number'; @@ -96,12 +94,12 @@ export class RateLimiterService { typeof limitation.max === 'number'; if (hasShortTermLimit) { - return min; + min(); } else if (hasLongTermLimit) { - return max; + max(); } else { - return Promise.resolve(); + ok(); } - } + }); } } diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 7c1bd942b1..fcdec234c2 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; -import { comparePassword, hashPassword, isOldAlgorithm } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import * as OTPAuth from 'otpauth'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; @@ -22,7 +22,7 @@ import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; -import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; +import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types'; import type { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() @@ -123,12 +123,7 @@ export class SigninApiService { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); // Compare password - const same = await comparePassword(password, profile.password!); - - if (same && isOldAlgorithm(profile.password!)) { - profile.password = await hashPassword(password); - await this.userProfilesRepository.save(profile); - } + const same = await bcrypt.compare(password, profile.password!); const fail = async (status?: number, failure?: { id: string }) => { // Append signin history diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index 70306c3113..4ab0f2d1cc 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -29,13 +29,13 @@ export class SigninService { public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) { setImmediate(async () => { // Append signin history - const record = await this.signinsRepository.insertOne({ + const record = await this.signinsRepository.insert({ id: this.idService.gen(), userId: user.id, ip: request.ip, headers: request.headers as any, success: true, - }); + }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); // Publish signin event this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index e5fa1fea3f..30612062b5 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -1,9 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js'; @@ -20,7 +21,6 @@ import { bindThis } from '@/decorators.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; -import { hashPassword } from '@/misc/password.js'; @Injectable() export class SignupApiService { @@ -65,7 +65,6 @@ export class SignupApiService { 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; - 'm-captcha-response'?: string; } }>, reply: FastifyReply, @@ -83,12 +82,6 @@ export class SignupApiService { }); } - if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) { - await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { - throw new FastifyReplyError(400, err); - }); - } - if (instance.enableRecaptcha && instance.recaptchaSecretKey) { await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); @@ -163,12 +156,12 @@ export class SignupApiService { } if (instance.emailRequiredForSignup) { - if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { + if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); } // Check deleted username duplication - if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) { + if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) { throw new FastifyReplyError(400, 'USED_USERNAME'); } @@ -180,15 +173,16 @@ export class SignupApiService { const code = secureRndstr(16, { chars: L_CHARS }); // Generate hash of password - const hash = await hashPassword(password); + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); - const pendingUser = await this.userPendingsRepository.insertOne({ + const pendingUser = await this.userPendingsRepository.insert({ id: this.idService.gen(), code, email: emailAddress!, username: username, password: hash, - }); + }).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0])); const link = `${this.config.url}/signup-complete/${code}`; @@ -212,7 +206,7 @@ export class SignupApiService { }); const res = await this.userEntityService.pack(account, account, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, }); diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 6726d7a462..04bc2a8827 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index f4c361f571..c188bef7d2 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 0ab006f7e6..e61d9e6d7e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -1,27 +1,18 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { permissions } from 'cherrypick-js'; -import type { KeyOf, Schema } from '@/misc/json-schema.js'; +import type { Schema } from '@/misc/json-schema.js'; +import { RolePolicies } from '@/core/RoleService.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_abuseReportResolver_create from '@/server/api/endpoints/admin/abuse-report-resolver/create.js'; -import * as ep___admin_abuseReportResolver_update from '@/server/api/endpoints/admin/abuse-report-resolver/update.js'; -import * as ep___admin_abuseReportResolver_delete from '@/server/api/endpoints/admin/abuse-report-resolver/delete.js'; -import * as ep___admin_abuseReportResolver_list from '@/server/api/endpoints/admin/abuse-report-resolver/list.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_abuseReportResolver_create from './endpoints/admin/abuse-report-resolver/create.js'; +import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-report-resolver/update.js'; +import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js'; +import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js'; +import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.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'; @@ -60,8 +51,7 @@ import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-li import * as ep___admin_emoji_steal from './endpoints/admin/emoji/steal.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_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'; @@ -88,8 +78,6 @@ 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_suspendUser from './endpoints/admin/suspend-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; -import * as ep___admin_setUserSensitive from './endpoints/admin/set-user-sensitive.js'; -import * as ep___admin_unsetUserSensitive from './endpoints/admin/unset-user-sensitive.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'; @@ -102,13 +90,7 @@ 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___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'; @@ -234,7 +216,6 @@ 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'; @@ -328,8 +309,6 @@ 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_delete from './endpoints/notifications/delete.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'; @@ -416,8 +395,6 @@ import * as ep___users_translate from './endpoints/users/translate.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___bubbleGame_register from './endpoints/bubble-game/register.js'; -import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -426,11 +403,6 @@ const eps = [ ['admin/abuse-report-resolver/delete', ep___admin_abuseReportResolver_delete], ['admin/abuse-report-resolver/update', ep___admin_abuseReportResolver_update], ['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], @@ -496,8 +468,6 @@ const eps = [ ['admin/show-users', ep___admin_showUsers], ['admin/suspend-user', ep___admin_suspendUser], ['admin/unsuspend-user', ep___admin_unsuspendUser], - ['admin/set-user-sensitive', ep___admin_setUserSensitive], - ['admin/unset-user-sensitive', ep___admin_unsetUserSensitive], ['admin/update-meta', ep___admin_updateMeta], ['admin/delete-account', ep___admin_deleteAccount], ['admin/update-user-note', ep___admin_updateUserNote], @@ -510,13 +480,7 @@ const eps = [ ['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], ['announcements', ep___announcements], - ['announcements/show', ep___announcements_show], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], ['antennas/list', ep___antennas_list], @@ -642,7 +606,6 @@ const eps = [ ['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], @@ -736,8 +699,6 @@ const eps = [ ['notes/unrenote', ep___notes_unrenote], ['notes/user-list-timeline', ep___notes_userListTimeline], ['notifications/create', ep___notifications_create], - ['notifications/delete', ep___notifications_delete], - ['notifications/flush', ep___notifications_flush], ['notifications/mark-all-as-read', ep___notifications_markAllAsRead], ['notifications/test-notification', ep___notifications_testNotification], ['page-push', ep___pagePush], @@ -824,8 +785,6 @@ const eps = [ ['fetch-rss', ep___fetchRss], ['fetch-external-resources', ep___fetchExternalResources], ['retention', ep___retention], - ['bubble-game/register', ep___bubbleGame_register], - ['bubble-game/ranking', ep___bubbleGame_ranking], ]; interface IEndpointMetaBase { @@ -859,7 +818,7 @@ interface IEndpointMetaBase { */ readonly requireAdmin?: boolean; - readonly requireRolePolicy?: KeyOf<'RolePolicies'>; + readonly requireRolePolicy?: keyof RolePolicies; /** * 引っ越し済みのユーザーによるリクエストを禁止するか @@ -953,12 +912,8 @@ export interface IEndpoint { const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { return { name: name, - get meta() { - return ep.meta ?? {}; - }, - get params() { - return ep.paramDef; - }, + get meta() { return ep.meta ?? {}; }, + get params() { return ep.paramDef; }, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/create.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/create.ts index 85ce3d51cf..a61337a15d 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,45 +13,41 @@ import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['admin'], + requireCredential: true, - secure: true, + requireAdmin: true, - kind: 'arr-create', // ここにkindプロパティを追加 + res: { type: 'object', properties: { name: { type: 'string', - nullable: false, - optional: false, + nullable: false, optional: false, }, targetUserPattern: { type: 'string', - nullable: true, - optional: false, + nullable: true, optional: false, }, reporterPattern: { type: 'string', - nullable: true, - optional: false, + nullable: true, optional: false, }, reportContentPattern: { type: 'string', - nullable: true, - optional: false, + nullable: true, optional: false, }, expiresAt: { type: 'string', - nullable: false, - optional: false, + nullable: false, optional: false, }, forward: { type: 'boolean', - nullable: false, - optional: false, + nullable: false, optional: false, }, }, }, + errors: { invalidRegularExpressionForTargetUser: { message: 'Invalid regular expression for target user.', @@ -127,7 +123,7 @@ export default class extends Endpoint { ps.expiresAt === '6months' ? function () { expirationDate!.setUTCMonth((expirationDate!.getUTCMonth() + 6 + 1) % 12 - 1); expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + (Math.floor((previousMonth + 6 + 1) / 12))); } : ps.expiresAt === '1year' ? function () { expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + 1); } : function () { expirationDate = null; })(); - return await this.abuseReportResolverRepository.insertOne({ + return await this.abuseReportResolverRepository.insert({ id: this.idService.gen(), updatedAt: now, name: ps.name, @@ -137,7 +133,7 @@ export default class extends Endpoint { expirationDate, expiresAt: ps.expiresAt, forward: ps.forward, - }); + }).then(x => this.abuseReportResolverRepository.findOneByOrFail(x.identifiers[0])); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/delete.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/delete.ts index ea32124320..d6df883d6f 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -11,9 +11,8 @@ import { ApiError } from '../../../error.js'; export const meta = { requireCrendential: true, - kind: 'arr-delete', // ここにkindプロパティを追加 + requireAdmin: true, - secure: true, errors: { resolverNotFound: { diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/list.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/list.ts index a5a6f3c54c..452b7255f2 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -12,8 +12,7 @@ import type { AbuseReportResolversRepository } from '@/models/_.js'; export const meta = { requireCredential: true, - kind: 'arr-list', // ここにkindプロパティを追加 - secure: true, + requireAdmin: true, res: { diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/update.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/update.ts index d0cd055444..c43b9b1c53 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report-resolver/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -12,8 +12,7 @@ import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - kind: 'arr-update', // ここにkindプロパティを追加 - secure: true, + requireAdmin: true, errors: { diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts deleted file mode 100644 index bdfbcba518..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ApiError } from '@/server/api/error.js'; -import { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { DI } from '@/di-symbols.js'; -import type { UserProfilesRepository } from '@/models/_.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:abuse-report:notification-recipient', - - res: { - type: 'object', - ref: 'AbuseReportNotificationRecipient', - }, - - errors: { - correlationCheckEmail: { - message: 'If "method" is email, "userId" must be set.', - code: 'CORRELATION_CHECK_EMAIL', - id: '348bb8ae-575a-6fe9-4327-5811999def8f', - httpStatusCode: 400, - }, - correlationCheckWebhook: { - message: 'If "method" is webhook, "systemWebhookId" must be set.', - code: 'CORRELATION_CHECK_WEBHOOK', - id: 'b0c15051-de2d-29ef-260c-9585cddd701a', - httpStatusCode: 400, - }, - emailAddressNotSet: { - message: 'Email address is not set.', - code: 'EMAIL_ADDRESS_NOT_SET', - id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f', - httpStatusCode: 400, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - isActive: { - type: 'boolean', - }, - name: { - type: 'string', - minLength: 1, - maxLength: 255, - }, - method: { - type: 'string', - enum: ['email', 'webhook'], - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - systemWebhookId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: [ - 'isActive', - 'name', - 'method', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private abuseReportNotificationService: AbuseReportNotificationService, - private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - if (ps.method === 'email') { - const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); - if (!ps.userId || !userProfile) { - throw new ApiError(meta.errors.correlationCheckEmail); - } - - if (!userProfile.email || !userProfile.emailVerified) { - throw new ApiError(meta.errors.emailAddressNotSet); - } - } - - if (ps.method === 'webhook' && !ps.systemWebhookId) { - throw new ApiError(meta.errors.correlationCheckWebhook); - } - - const userId = ps.method === 'email' ? ps.userId : null; - const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null; - const result = await this.abuseReportNotificationService.createRecipient( - { - isActive: ps.isActive, - name: ps.name, - method: ps.method, - userId: userId ?? null, - systemWebhookId: systemWebhookId ?? null, - }, - me, - ); - - return this.abuseReportNotificationRecipientEntityService.pack(result); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts deleted file mode 100644 index b6dc44e09c..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:abuse-report:notification-recipient', -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - }, - required: [ - 'id', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private abuseReportNotificationService: AbuseReportNotificationService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.abuseReportNotificationService.deleteRecipient( - ps.id, - me, - ); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts deleted file mode 100644 index dad9161a8a..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'read:admin:abuse-report:notification-recipient', - - res: { - type: 'array', - items: { - type: 'object', - ref: 'AbuseReportNotificationRecipient', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - method: { - type: 'array', - items: { - type: 'string', - enum: ['email', 'webhook'], - }, - }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private abuseReportNotificationService: AbuseReportNotificationService, - private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, - ) { - super(meta, paramDef, async (ps) => { - const recipients = await this.abuseReportNotificationService.fetchRecipients({ method: ps.method }); - return this.abuseReportNotificationRecipientEntityService.packMany(recipients); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts deleted file mode 100644 index 557798f946..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { ApiError } from '@/server/api/error.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'read:admin:abuse-report:notification-recipient', - - res: { - type: 'object', - ref: 'AbuseReportNotificationRecipient', - }, - - errors: { - noSuchRecipient: { - message: 'No such recipient.', - code: 'NO_SUCH_RECIPIENT', - id: '013de6a8-f757-04cb-4d73-cc2a7e3368e4', - kind: 'server', - httpStatusCode: 404, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['id'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private abuseReportNotificationService: AbuseReportNotificationService, - private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, - ) { - super(meta, paramDef, async (ps) => { - const recipients = await this.abuseReportNotificationService.fetchRecipients({ ids: [ps.id] }); - if (recipients.length === 0) { - throw new ApiError(meta.errors.noSuchRecipient); - } - - return this.abuseReportNotificationRecipientEntityService.pack(recipients[0]); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts deleted file mode 100644 index bd4b485217..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ApiError } from '@/server/api/error.js'; -import { - AbuseReportNotificationRecipientEntityService, -} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { DI } from '@/di-symbols.js'; -import type { UserProfilesRepository } from '@/models/_.js'; - -export const meta = { - tags: ['admin', 'abuse-report', 'notification-recipient'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:abuse-report:notification-recipient', - - res: { - type: 'object', - ref: 'AbuseReportNotificationRecipient', - }, - - errors: { - correlationCheckEmail: { - message: 'If "method" is email, "userId" must be set.', - code: 'CORRELATION_CHECK_EMAIL', - id: '348bb8ae-575a-6fe9-4327-5811999def8f', - httpStatusCode: 400, - }, - correlationCheckWebhook: { - message: 'If "method" is webhook, "systemWebhookId" must be set.', - code: 'CORRELATION_CHECK_WEBHOOK', - id: 'b0c15051-de2d-29ef-260c-9585cddd701a', - httpStatusCode: 400, - }, - emailAddressNotSet: { - message: 'Email address is not set.', - code: 'EMAIL_ADDRESS_NOT_SET', - id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f', - httpStatusCode: 400, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - isActive: { - type: 'boolean', - }, - name: { - type: 'string', - minLength: 1, - maxLength: 255, - }, - method: { - type: 'string', - enum: ['email', 'webhook'], - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - systemWebhookId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: [ - 'id', - 'isActive', - 'name', - 'method', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private abuseReportNotificationService: AbuseReportNotificationService, - private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - if (ps.method === 'email') { - const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); - if (!ps.userId || !userProfile) { - throw new ApiError(meta.errors.correlationCheckEmail); - } - - if (!userProfile.email || !userProfile.emailVerified) { - throw new ApiError(meta.errors.emailAddressNotSet); - } - } - - if (ps.method === 'webhook' && !ps.systemWebhookId) { - throw new ApiError(meta.errors.correlationCheckWebhook); - } - - const userId = ps.method === 'email' ? ps.userId : null; - const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null; - const result = await this.abuseReportNotificationService.updateRecipient( - { - id: ps.id, - isActive: ps.isActive, - name: ps.name, - method: ps.method, - userId: userId ?? null, - systemWebhookId: systemWebhookId ?? null, - }, - me, - ); - - return this.abuseReportNotificationRecipientEntityService.pack(result); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index cf3f257ca6..f07972c331 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -62,17 +62,17 @@ export const meta = { reporter: { type: 'object', nullable: false, optional: false, - ref: 'UserDetailedNotMe', + ref: 'User', }, targetUser: { type: 'object', nullable: false, optional: false, - ref: 'UserDetailedNotMe', + ref: 'User', }, assignee: { type: 'object', nullable: true, optional: true, - ref: 'UserDetailedNotMe', + ref: 'User', }, }, }, 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 a7e8a3b018..569baad0f3 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -9,10 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; -import { Packed } from '@/misc/json-schema.js'; export const meta = { tags: ['admin'], @@ -20,7 +18,7 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'MeDetailed', + ref: 'User', properties: { token: { type: 'string', @@ -47,12 +45,13 @@ export default class extends Endpoint { // eslint- private userEntityService: UserEntityService, private signupService: SignupService, - private instanceActorService: InstanceActorService, ) { super(meta, paramDef, async (ps, _me, token) => { const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; - const realUsers = await this.instanceActorService.realLocalUsersPresent(); - if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied'); + const noUsers = (await this.usersRepository.countBy({ + host: IsNull(), + })) === 0; + if ((!noUsers && !me?.isRoot) || token !== null) throw new Error('access denied'); const { account, secret } = await this.signupService.signup({ username: ps.username, @@ -61,11 +60,11 @@ export default class extends Endpoint { // eslint- }); const res = await this.userEntityService.pack(account, account, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, - }) as Packed<'MeDetailed'> & { token: string }; + }); - res.token = secret; + (res as any).token = secret; return res; }); diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 4074e416b8..a93e1c0fa4 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts index 12cd5cf295..70bf985709 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -27,7 +27,7 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'User', }, } as const; @@ -58,7 +58,7 @@ export default class extends Endpoint { // eslint- } const res = await this.userEntityService.pack(profile.user!, null, { - schema: 'UserDetailedNotMe', + detail: true, }); return res; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index 955154f4fb..852a558e41 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -50,7 +50,7 @@ export default class extends Endpoint { // eslint- private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { - const ad = await this.adsRepository.insertOne({ + const ad = await this.adsRepository.insert({ id: this.idService.gen(), expiresAt: new Date(ps.expiresAt), startsAt: new Date(ps.startsAt), @@ -61,7 +61,7 @@ export default class extends Endpoint { // eslint- ratio: ps.ratio, place: ps.place, memo: ps.memo, - }); + }).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id })); this.moderationLogService.log(me, 'createAd', { adId: ad.id, diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts index 501e13c6a7..9bc4a25888 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index 6406709cda..0d1eee847d 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 4e3d731aca..dfe6e55e51 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -40,7 +40,7 @@ export const paramDef = { startsAt: { type: 'integer' }, dayOfWeek: { type: 'integer' }, }, - required: ['id'], + required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'], } as const; @Injectable() @@ -63,8 +63,8 @@ export default class extends Endpoint { // eslint- ratio: ps.ratio, memo: ps.memo, imageUrl: ps.imageUrl, - expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined, - startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined, + expiresAt: new Date(ps.expiresAt), + startsAt: new Date(ps.startsAt), dayOfWeek: ps.dayOfWeek, }); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 2dae1df87d..603e9a2c96 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts index 6d1e1b0a10..c7e6aa1e39 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 7596bf44e3..ae458afa35 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -69,7 +69,6 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id', nullable: true }, - status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' }, }, required: [], } as const; @@ -88,13 +87,7 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - - if (ps.status === 'archived') { - query.andWhere('announcement.isActive = false'); - } else if (ps.status === 'active') { - query.andWhere('announcement.isActive = true'); - } - + query.andWhere('announcement.isActive = true'); if (ps.userId) { query.andWhere('announcement.userId = :userId', { userId: ps.userId }); } else { diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 6fce6e4e0a..53b681f94f 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index fd21309818..59cec7f075 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts index 3a5673d99d..d5af225e9c 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index aee90023e1..c171c78580 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts index 34b3b5a11f..2dbad159cf 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index b6f0f22d60..b6be624545 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,6 +15,9 @@ export const meta = { requireCredential: true, requireAdmin: true, kind: 'write:admin:delete-account', + + res: { + }, } as const; export const paramDef = { 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 d8341b3ad7..02dcaa9c0c 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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..76fec430c6 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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..5fc776e8c7 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index 915d777e77..c8bacf428d 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index a7136d8c8c..0d35784c04 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -61,7 +61,7 @@ export const meta = { name: { type: 'string', optional: false, nullable: false, - example: '192.jpg', + example: 'lenna.jpg', }, type: { type: 'string', @@ -84,24 +84,6 @@ export const meta = { properties: { type: 'object', optional: false, nullable: false, - properties: { - width: { - type: 'number', - optional: true, nullable: false, - }, - height: { - type: 'number', - optional: true, nullable: false, - }, - orientation: { - type: 'number', - optional: true, nullable: false, - }, - avgColor: { - type: 'string', - optional: true, nullable: false, - }, - }, }, storedInternal: { type: 'boolean', 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 a30a080e59..46f5a2dc32 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 796f273330..4ad3691da4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -31,10 +31,7 @@ export const meta = { }, }, - res: { - type: 'object', - ref: 'EmojiDetailed', - }, + ref: 'EmojiDetailed', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/adds.ts b/packages/backend/src/server/api/endpoints/admin/emoji/adds.ts index 4ca78e16d9..b7e38e3ff1 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/adds.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/adds.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -32,10 +32,7 @@ export const meta = { }, }, - res: { - type: 'object', - ref: 'EmojiDetailed', - }, + ref: 'EmojiDetailed', } as const; export const paramDef = { 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 975f892df9..6f9b43d8e4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index cec9f700c3..47d0cc605a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 50c45b6ac5..388e34a581 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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..d8f0c7b187 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 0889ceb76f..d24fb1025a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index ffb5dbf4b5..4b39631c0c 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 0fa119eabe..9318d8ae9f 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 d9ee18699c..2ac4da8748 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 dc25df2767..4ae6d7edfc 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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..31ec9b8e4e 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/steal.ts b/packages/backend/src/server/api/endpoints/admin/emoji/steal.ts index a22d784d14..a9d9dd24bc 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/steal.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/steal.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 22609a16a3..93f3fdae77 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -57,10 +57,7 @@ export const paramDef = { type: 'string', } }, }, - anyOf: [ - { required: ['id'] }, - { required: ['name'] }, - ], + required: ['id', 'name', 'aliases'], } as const; @Injectable() @@ -73,33 +70,27 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { let driveFile; + if (ps.fileId) { driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } - - let emojiId; - if (ps.id) { - emojiId = ps.id; - const emoji = await this.customEmojiService.getEmojiById(ps.id); - if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); - if (ps.name && (ps.name !== emoji.name)) { + const emoji = await this.customEmojiService.getEmojiById(ps.id); + if (emoji != null) { + if (ps.name !== emoji.name) { const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); } } else { - if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.'); - const emoji = await this.customEmojiService.getEmojiByName(ps.name); - if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); - emojiId = emoji.id; + throw new ApiError(meta.errors.noSuchEmoji); } - await this.customEmojiService.update(emojiId, { + await this.customEmojiService.update(ps.id, { driveFile, name: ps.name, - category: ps.category, + category: ps.category ?? null, aliases: ps.aliases, - license: ps.license, + license: ps.license ?? null, isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, 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..7b6b024f40 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index 556e291025..ab9dd7194f 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 9e93310746..4ba89eaa38 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 fed7bfbbde..afba344966 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -24,9 +24,8 @@ export const paramDef = { properties: { host: { type: 'string' }, isSuspended: { type: 'boolean' }, - moderationNote: { type: 'string' }, }, - required: ['host'], + required: ['host', 'isSuspended'], } as const; @Injectable() @@ -46,19 +45,11 @@ export default class extends Endpoint { // eslint- throw new Error('instance not found'); } - const isSuspendedBefore = instance.suspensionState !== 'none'; - let suspensionState: undefined | 'manuallySuspended' | 'none'; - - if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) { - suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none'; - } - await this.federatedInstanceService.update(instance.id, { - suspensionState, - moderationNote: ps.moderationNote, + isSuspended: ps.isSuspended, }); - if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) { + if (instance.isSuspended !== ps.isSuspended) { if (ps.isSuspended) { this.moderationLogService.log(me, 'suspendRemoteInstance', { id: instance.id, @@ -71,15 +62,6 @@ export default class extends Endpoint { // eslint- }); } } - - if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) { - this.moderationLogService.log(me, 'updateRemoteInstanceNote', { - id: instance.id, - host: instance.host, - before: instance.moderationNote, - after: ps.moderationNote, - }); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts index 90a3fa0200..1ce08d1fcd 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts index eb85fca179..4db012b35b 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -18,18 +18,6 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - additionalProperties: { - type: 'object', - properties: { - count: { - type: 'number', - }, - size: { - type: 'number', - }, - }, - required: ['count', 'size'], - }, example: { migrations: { count: 66, diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts index b7781b8c99..7fa3159a85 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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..9bbb1b41f2 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -66,11 +66,11 @@ export default class extends Endpoint { // eslint- const ticketsPromises = []; for (let i = 0; i < ps.count; i++) { - ticketsPromises.push(this.registrationTicketsRepository.insertOne({ + ticketsPromises.push(this.registrationTicketsRepository.insert({ id: this.idService.gen(), expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, code: generateInviteCode(), - })); + }).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]))); } const tickets = await Promise.all(ticketsPromises); diff --git a/packages/backend/src/server/api/endpoints/admin/invite/list.ts b/packages/backend/src/server/api/endpoints/admin/invite/list.ts index e33a9a1aec..6a11418dd3 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/invite/revoke.ts b/packages/backend/src/server/api/endpoints/admin/invite/revoke.ts index c7db1eae65..83ee1d11bf 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/revoke.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/revoke.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index ce0fcb4110..20658a74a8 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -41,18 +41,6 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - enableMcaptcha: { - type: 'boolean', - optional: false, nullable: false, - }, - mcaptchaSiteKey: { - type: 'string', - optional: false, nullable: true, - }, - mcaptchaInstanceUrl: { - type: 'string', - optional: false, nullable: true, - }, enableRecaptcha: { type: 'boolean', optional: false, nullable: false, @@ -132,16 +120,6 @@ export const meta = { nullable: false, }, }, - mediaSilencedHosts: { - type: 'array', - optional: false, - nullable: false, - items: { - type: 'string', - optional: false, - nullable: false, - }, - }, pinnedUsers: { type: 'array', optional: false, nullable: false, @@ -170,13 +148,6 @@ export const meta = { type: 'string', }, }, - prohibitedWords: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - }, - }, bannedEmailDomains: { type: 'array', optional: true, nullable: false, @@ -196,10 +167,6 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - mcaptchaSecretKey: { - type: 'string', - optional: false, nullable: true, - }, recaptchaSecretKey: { type: 'string', optional: false, nullable: true, @@ -369,18 +336,6 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - enableTruemailApi: { - type: 'boolean', - optional: false, nullable: false, - }, - truemailInstance: { - type: 'string', - optional: false, nullable: true, - }, - truemailAuthKey: { - type: 'string', - optional: false, nullable: true, - }, enableChartsForRemoteUser: { type: 'boolean', optional: false, nullable: false, @@ -493,19 +448,13 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - inquiryUrl: { - type: 'string', - optional: false, nullable: true, - }, repositoryUrl: { type: 'string', - optional: false, nullable: true, + optional: false, nullable: false, }, summalyProxy: { type: 'string', optional: false, nullable: true, - deprecated: true, - description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', }, themeColor: { type: 'string', @@ -523,30 +472,6 @@ export const meta = { type: 'string', optional: false, nullable: false, }, - urlPreviewEnabled: { - type: 'boolean', - optional: false, nullable: false, - }, - urlPreviewTimeout: { - type: 'number', - optional: false, nullable: false, - }, - urlPreviewMaximumContentLength: { - type: 'number', - optional: false, nullable: false, - }, - urlPreviewRequireContentLength: { - type: 'boolean', - optional: false, nullable: false, - }, - urlPreviewUserAgent: { - type: 'string', - optional: false, nullable: true, - }, - urlPreviewSummaryProxyUrl: { - type: 'string', - optional: false, nullable: true, - }, doNotSendNotificationEmailsForAbuseReport: { type: 'boolean', optional: false, nullable: false, @@ -604,15 +529,10 @@ export default class extends Endpoint { // eslint- feedbackUrl: instance.feedbackUrl, impressumUrl: instance.impressumUrl, privacyPolicyUrl: instance.privacyPolicyUrl, - statusUrl: instance.statusUrl, - inquiryUrl: instance.inquiryUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableMcaptcha: instance.enableMcaptcha, - mcaptchaSiteKey: instance.mcaptchaSitekey, - mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, enableRecaptcha: instance.enableRecaptcha, recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, @@ -644,12 +564,9 @@ export default class extends Endpoint { // eslint- hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, silencedHosts: instance.silencedHosts, - mediaSilencedHosts: instance.mediaSilencedHosts, sensitiveWords: instance.sensitiveWords, - prohibitedWords: instance.prohibitedWords, preservedUsernames: instance.preservedUsernames, hcaptchaSecretKey: instance.hcaptchaSecretKey, - mcaptchaSecretKey: instance.mcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, sensitiveMediaDetection: instance.sensitiveMediaDetection, @@ -657,6 +574,7 @@ export default class extends Endpoint { // eslint- setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, proxyAccountId: instance.proxyAccountId, + summalyProxy: instance.summalyProxy, email: instance.email, smtpSecure: instance.smtpSecure, smtpHost: instance.smtpHost, @@ -701,9 +619,6 @@ export default class extends Endpoint { // eslint- enableActiveEmailValidation: instance.enableActiveEmailValidation, enableVerifymailApi: instance.enableVerifymailApi, verifymailAuthKey: instance.verifymailAuthKey, - enableTruemailApi: instance.enableTruemailApi, - truemailInstance: instance.truemailInstance, - truemailAuthKey: instance.truemailAuthKey, enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, enableServerMachineStats: instance.enableServerMachineStats, @@ -718,14 +633,6 @@ export default class extends Endpoint { // eslint- perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, notesPerOneAd: instance.notesPerOneAd, - summalyProxy: instance.urlPreviewSummaryProxyUrl, - urlPreviewEnabled: instance.urlPreviewEnabled, - urlPreviewTimeout: instance.urlPreviewTimeout, - urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength, - urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, - urlPreviewUserAgent: instance.urlPreviewUserAgent, - urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, - urlPreviewDirectSummalyProxy: instance.directSummalyProxy, doNotSendNotificationEmailsForAbuseReport: instance.doNotSendNotificationEmailsForAbuseReport, emailToReceiveAbuseReport: instance.emailToReceiveAbuseReport, enableReceivePrerelease: instance.enableReceivePrerelease, 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..ac6770a4f2 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -55,7 +55,7 @@ export default class extends Endpoint { // eslint- throw e; }); - const exist = await this.promoNotesRepository.exists({ where: { noteId: note.id } }); + const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } }); if (exist) { throw new ApiError(meta.errors.alreadyPromoted); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts index 3f7df0e63d..9e5f597b80 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index acc1554289..65d29c7b13 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index add65fe335..ed32d0f1a9 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts index 7502d4e1f7..fa9805038c 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index d7f9e4eaa3..34da300c90 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], @@ -53,8 +53,7 @@ export default class extends Endpoint { // eslint- @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) { super(meta, paramDef, async (ps, me) => { const deliverJobCounts = await this.deliverQueue.getJobCounts(); 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..240a076320 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts index 587d5c3b03..d764e3c17f 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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..8fd8725e41 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index 23c4c4a0d5..eb7034c05c 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -1,15 +1,15 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { hashPassword } from '@/misc/password.js'; export const meta = { tags: ['admin'], @@ -25,8 +25,8 @@ export const meta = { password: { type: 'string', optional: false, nullable: false, - minLength: 16, - maxLength: 16, + minLength: 8, + maxLength: 8, }, }, }, @@ -62,10 +62,10 @@ export default class extends Endpoint { // eslint- throw new Error('cannot reset password of root'); } - const passwd = secureRndstr(16); + const passwd = secureRndstr(8); // Generate hash of password - const hash = await hashPassword(passwd); + const hash = bcrypt.hashSync(passwd); await this.userProfilesRepository.update({ userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index 9b79100fcf..5f6bce6b24 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -1,14 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; +import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js'; +import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -16,16 +18,6 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'write:admin:resolve-abuse-user-report', - - errors: { - noSuchAbuseReport: { - message: 'No such abuse report.', - code: 'NO_SUCH_ABUSE_REPORT', - id: 'ac3794dd-2ce4-d878-e546-73c60c06b398', - kind: 'server', - httpStatusCode: 404, - }, - }, } as const; export const paramDef = { @@ -37,20 +29,47 @@ export const paramDef = { required: ['reportId'], } as const; +// TODO: ロジックをサービスに切り出す + @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, - private abuseReportService: AbuseReportService, + + private queueService: QueueService, + private instanceActorService: InstanceActorService, + private apRendererService: ApRendererService, + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); - if (!report) { - throw new ApiError(meta.errors.noSuchAbuseReport); + + if (report == null) { + throw new Error('report not found'); } - await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me); + if (ps.forward && report.targetUserHost != null) { + const actor = await this.instanceActorService.getInstanceActor(); + const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); + + this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false); + } + + await this.abuseUserReportsRepository.update(report.id, { + resolved: true, + assigneeId: me.id, + forwarded: ps.forward && report.targetUserHost != null, + }); + + this.moderationLogService.log(me, 'resolveAbuseReport', { + reportId: report.id, + report: report, + forwarded: ps.forward && report.targetUserHost != null, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts index b6c7953781..13b6692a06 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index e0c02f7a5d..3d252eeab4 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts index 638e2b15b9..2a6ebea34b 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts index 333fac6aa6..c86e32d6d5 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts index 13e5cbb995..5350c95594 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts index e7da3384b1..061e934547 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts index d7209965db..411c87f5a7 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index 465ad7aaaf..582b9cc739 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -1,11 +1,12 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { RolesRepository } from '@/models/_.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleService } from '@/core/RoleService.js'; @@ -49,6 +50,19 @@ export const paramDef = { }, required: [ 'roleId', + 'name', + 'description', + 'color', + 'iconUrl', + 'target', + 'condFormula', + 'isPublic', + 'isModerator', + 'isAdministrator', + 'asBadge', + 'canEditMembersByModerator', + 'displayOrder', + 'policies', ], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index 198166bec2..7de98434ff 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -17,7 +17,7 @@ export const meta = { tags: ['admin', 'role', 'users'], requireCredential: false, - requireModerator: true, + requireAdmin: true, kind: 'read:admin:roles', errors: { @@ -89,13 +89,10 @@ export default class extends Endpoint { // eslint- .limit(ps.limit) .getMany(); - const _users = assigns.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) - .then(users => new Map(users.map(u => [u.id, u]))); return await Promise.all(assigns.map(async assign => ({ id: assign.id, createdAt: this.idService.parse(assign.id).date.toISOString(), - user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), + user: await this.userEntityService.pack(assign.user!, me, { detail: true }), expiresAt: assign.expiresAt?.toISOString() ?? null, }))); }); diff --git a/packages/backend/src/server/api/endpoints/admin/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts index f01a7778a8..ed1f125695 100644 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts index 80b6a4d32e..15e26f5d3c 100644 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/set-user-sensitive.ts b/packages/backend/src/server/api/endpoints/admin/set-user-sensitive.ts deleted file mode 100644 index 6fe1b60ac7..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/set-user-sensitive.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { RoleService } from '@/core/RoleService.js'; -import { QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:suspend-user', -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - private roleService: RoleService, - private moderationLogService: ModerationLogService, - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (await this.roleService.isAdministrator(user)) { - throw new Error('cannot set admin as sensitive'); - } - - await this.userProfilesRepository.update(user.id, { - isSensitive: true, - }); - - await this.usersRepository.update(user.id, { - isSensitive: true, - }); - - this.moderationLogService.log(me, 'setSensitive', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - }); - }) - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index 58c5f1f60a..d70ddd021a 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -50,7 +50,7 @@ export const meta = { user: { type: 'object', optional: false, nullable: false, - ref: 'UserDetailedNotMe', + ref: 'UserDetailed', }, }, }, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 5a1a7a8a31..39a91b7a69 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,6 @@ import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { notificationRecieveConfig } from '@/models/json-schema/user.js'; export const meta = { tags: ['admin'], @@ -22,162 +21,6 @@ export const meta = { res: { type: 'object', nullable: false, optional: false, - properties: { - email: { - type: 'string', - optional: false, nullable: true, - }, - emailVerified: { - type: 'boolean', - optional: false, nullable: false, - }, - autoAcceptFollowed: { - type: 'boolean', - optional: false, nullable: false, - }, - noCrawle: { - type: 'boolean', - optional: false, nullable: false, - }, - preventAiLearning: { - type: 'boolean', - optional: false, nullable: false, - }, - alwaysMarkNsfw: { - type: 'boolean', - optional: false, nullable: false, - }, - autoSensitive: { - type: 'boolean', - optional: false, nullable: false, - }, - carefulBot: { - type: 'boolean', - optional: false, nullable: false, - }, - injectFeaturedNote: { - type: 'boolean', - optional: false, nullable: false, - }, - receiveAnnouncementEmail: { - type: 'boolean', - optional: false, nullable: false, - }, - mutedWords: { - type: 'array', - optional: false, nullable: false, - items: { - anyOf: [ - { - type: 'string', - }, - { - type: 'array', - items: { - type: 'string', - }, - }, - ], - }, - }, - mutedInstances: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - }, - }, - notificationRecieveConfig: { - type: 'object', - optional: false, nullable: false, - properties: { - note: { optional: true, ...notificationRecieveConfig }, - follow: { optional: true, ...notificationRecieveConfig }, - mention: { optional: true, ...notificationRecieveConfig }, - reply: { optional: true, ...notificationRecieveConfig }, - renote: { optional: true, ...notificationRecieveConfig }, - quote: { optional: true, ...notificationRecieveConfig }, - reaction: { optional: true, ...notificationRecieveConfig }, - pollEnded: { optional: true, ...notificationRecieveConfig }, - receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, - followRequestAccepted: { optional: true, ...notificationRecieveConfig }, - groupInvited: { optional: true, ...notificationRecieveConfig }, - roleAssigned: { optional: true, ...notificationRecieveConfig }, - achievementEarned: { optional: true, ...notificationRecieveConfig }, - app: { optional: true, ...notificationRecieveConfig }, - test: { optional: true, ...notificationRecieveConfig }, - }, - }, - isModerator: { - type: 'boolean', - optional: false, nullable: false, - }, - isSilenced: { - type: 'boolean', - optional: false, nullable: false, - }, - isSuspended: { - type: 'boolean', - optional: false, nullable: false, - }, - isSensitive: { - type: 'boolean', - optional: false, nullable: false, - }, - isHibernated: { - type: 'boolean', - optional: false, nullable: false, - }, - lastActiveDate: { - type: 'string', - optional: false, nullable: true, - }, - moderationNote: { - type: 'string', - optional: false, nullable: false, - }, - signins: { - type: 'array', - optional: false, nullable: false, - items: { - ref: 'Signin', - }, - }, - policies: { - type: 'object', - optional: false, nullable: false, - ref: 'RolePolicies', - }, - roles: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - ref: 'Role', - }, - }, - roleAssigns: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - properties: { - createdAt: { - type: 'string', - optional: false, nullable: false, - }, - expiresAt: { - type: 'string', - optional: false, nullable: true, - }, - roleId: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, - }, - }, }, } as const; @@ -245,9 +88,8 @@ export default class extends Endpoint { // eslint- isModerator: isModerator, isSilenced: isSilenced, isSuspended: user.isSuspended, - isSensitive: profile.isSensitive, isHibernated: user.isHibernated, - lastActiveDate: user.lastActiveDate ? user.lastActiveDate.toISOString() : null, + lastActiveDate: user.lastActiveDate, moderationNote: profile.moderationNote ?? '', signins, policies: await this.roleService.getUserPolicies(user.id), diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 2fef9abbf9..ca705ca307 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -16,7 +16,7 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:show-user', + kind: 'read:admin:show-users', res: { type: 'array', @@ -114,7 +114,7 @@ export default class extends Endpoint { // eslint- const users = await query.getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 8a946405cc..0e1df29db9 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts deleted file mode 100644 index 28071e7a33..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', - - res: { - type: 'object', - ref: 'SystemWebhook', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - isActive: { - type: 'boolean', - }, - name: { - type: 'string', - minLength: 1, - maxLength: 255, - }, - on: { - type: 'array', - items: { - type: 'string', - enum: systemWebhookEventTypes, - }, - }, - url: { - type: 'string', - minLength: 1, - maxLength: 1024, - }, - secret: { - type: 'string', - minLength: 1, - maxLength: 1024, - }, - }, - required: [ - 'isActive', - 'name', - 'on', - 'url', - 'secret', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const result = await this.systemWebhookService.createSystemWebhook( - { - isActive: ps.isActive, - name: ps.name, - on: ps.on, - url: ps.url, - secret: ps.secret, - }, - me, - ); - - return this.systemWebhookEntityService.pack(result); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts deleted file mode 100644 index 9cdfc7e70f..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - }, - required: [ - 'id', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - ) { - super(meta, paramDef, async (ps, me) => { - await this.systemWebhookService.deleteSystemWebhook( - ps.id, - me, - ); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts deleted file mode 100644 index 7a440a774e..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', - - res: { - type: 'array', - items: { - type: 'object', - ref: 'SystemWebhook', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - isActive: { - type: 'boolean', - }, - on: { - type: 'array', - items: { - type: 'string', - enum: systemWebhookEventTypes, - }, - }, - }, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - super(meta, paramDef, async (ps) => { - const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ - isActive: ps.isActive, - on: ps.on, - }); - return this.systemWebhookEntityService.packMany(webhooks); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts deleted file mode 100644 index 75862c96a7..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { ApiError } from '@/server/api/error.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', - - res: { - type: 'object', - ref: 'SystemWebhook', - }, - - errors: { - noSuchSystemWebhook: { - message: 'No such SystemWebhook.', - code: 'NO_SUCH_SYSTEM_WEBHOOK', - id: '38dd1ffe-04b4-6ff5-d8ba-4e6a6ae22c9d', - kind: 'server', - httpStatusCode: 404, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['id'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - super(meta, paramDef, async (ps) => { - const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [ps.id] }); - if (webhooks.length === 0) { - throw new ApiError(meta.errors.noSuchSystemWebhook); - } - - return this.systemWebhookEntityService.pack(webhooks[0]); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts deleted file mode 100644 index 8d68bb8f87..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; -import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; - -export const meta = { - tags: ['admin', 'system-webhook'], - - requireCredential: true, - requireModerator: true, - secure: true, - kind: 'write:admin:system-webhook', - - res: { - type: 'object', - ref: 'SystemWebhook', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { - type: 'string', - format: 'misskey:id', - }, - isActive: { - type: 'boolean', - }, - name: { - type: 'string', - minLength: 1, - maxLength: 255, - }, - on: { - type: 'array', - items: { - type: 'string', - enum: systemWebhookEventTypes, - }, - }, - url: { - type: 'string', - minLength: 1, - maxLength: 1024, - }, - secret: { - type: 'string', - minLength: 1, - maxLength: 1024, - }, - }, - required: [ - 'id', - 'isActive', - 'name', - 'on', - 'url', - 'secret', - ], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private systemWebhookService: SystemWebhookService, - private systemWebhookEntityService: SystemWebhookEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const result = await this.systemWebhookService.updateSystemWebhook( - { - id: ps.id, - isActive: ps.isActive, - name: ps.name, - on: ps.on, - url: ps.url, - secret: ps.secret, - }, - me, - ); - - return this.systemWebhookEntityService.pack(result); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts index ddab6f3a9d..65ecade35d 100644 --- a/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts index e16dad719c..31c3879b6b 100644 --- a/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-sensitive.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-sensitive.ts deleted file mode 100644 index 8e87776f1c..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/unset-user-sensitive.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import { RoleService } from '@/core/RoleService.js'; -import { QueueService } from '@/core/QueueService.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - kind: 'write:admin:suspend-user', -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - private roleService: RoleService, - private moderationLogService: ModerationLogService, - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error('user not found'); - } - - if (await this.roleService.isAdministrator(user)) { - throw new Error('cannot set admin as sensitive'); - } - - await this.userProfilesRepository.update(user.id, { - isSensitive: false, - }); - - await this.usersRepository.update(user.id, { - isSensitive: false, - }); - - this.moderationLogService.log(me, 'setSensitive', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - }); - }) - } -} diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 2c2b1bf6f5..3e9095d8b2 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 8b36af3e5a..24effdb209 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -43,11 +43,6 @@ export const paramDef = { type: 'string', }, }, - prohibitedWords: { - type: 'array', nullable: true, items: { - type: 'string', - }, - }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, @@ -70,10 +65,6 @@ export const paramDef = { enableHcaptcha: { type: 'boolean' }, hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSecretKey: { type: 'string', nullable: true }, - enableMcaptcha: { type: 'boolean' }, - mcaptchaSiteKey: { type: 'string', nullable: true }, - mcaptchaInstanceUrl: { type: 'string', nullable: true }, - mcaptchaSecretKey: { type: 'string', nullable: true }, enableRecaptcha: { type: 'boolean' }, recaptchaSiteKey: { type: 'string', nullable: true }, recaptchaSecretKey: { type: 'string', nullable: true }, @@ -92,6 +83,7 @@ export const paramDef = { type: 'string', }, }, + summalyProxy: { type: 'string', nullable: true }, translatorType: { type: 'string', nullable: true }, deeplAuthKey: { type: 'string', nullable: true }, deeplIsPro: { type: 'boolean' }, @@ -111,12 +103,10 @@ export const paramDef = { swPublicKey: { type: 'string', nullable: true }, swPrivateKey: { type: 'string', nullable: true }, tosUrl: { type: 'string', nullable: true }, - repositoryUrl: { type: 'string', nullable: true }, - feedbackUrl: { type: 'string', nullable: true }, + repositoryUrl: { type: 'string' }, + feedbackUrl: { type: 'string' }, impressumUrl: { type: 'string', nullable: true }, privacyPolicyUrl: { type: 'string', nullable: true }, - statusUrl: { type: 'string', nullable: true }, - inquiryUrl: { type: 'string', nullable: true }, useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, @@ -147,9 +137,6 @@ export const paramDef = { enableActiveEmailValidation: { type: 'boolean' }, enableVerifymailApi: { type: 'boolean' }, verifymailAuthKey: { type: 'string', nullable: true }, - enableTruemailApi: { type: 'boolean' }, - truemailInstance: { type: 'string', nullable: true }, - truemailAuthKey: { type: 'string', nullable: true }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, enableServerMachineStats: { type: 'boolean' }, @@ -172,24 +159,6 @@ export const paramDef = { type: 'string', }, }, - mediaSilencedHosts: { - type: 'array', - nullable: true, - items: { - type: 'string', - }, - }, - summalyProxy: { - type: 'string', nullable: true, - description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', - }, - urlPreviewEnabled: { type: 'boolean' }, - urlPreviewTimeout: { type: 'integer' }, - urlPreviewMaximumContentLength: { type: 'integer' }, - urlPreviewRequireContentLength: { type: 'boolean' }, - urlPreviewUserAgent: { type: 'string', nullable: true }, - urlPreviewSummaryProxyUrl: { type: 'string', nullable: true }, - urlPreviewDirectSummalyProxy: { type: 'boolean' }, doNotSendNotificationEmailsForAbuseReport: { type: 'boolean' }, emailToReceiveAbuseReport: { type: 'string', nullable: true }, enableReceivePrerelease: { type: 'boolean' }, @@ -228,9 +197,6 @@ export default class extends Endpoint { // eslint- if (Array.isArray(ps.sensitiveWords)) { set.sensitiveWords = ps.sensitiveWords.filter(Boolean); } - if (Array.isArray(ps.prohibitedWords)) { - set.prohibitedWords = ps.prohibitedWords.filter(Boolean); - } if (Array.isArray(ps.silencedHosts)) { let lastValue = ''; set.silencedHosts = ps.silencedHosts.sort().filter((h) => { @@ -239,14 +205,6 @@ export default class extends Endpoint { // eslint- return h !== '' && h !== lv && !set.blockedHosts?.includes(h); }); } - if (Array.isArray(ps.mediaSilencedHosts)) { - let lastValue = ''; - set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => { - const lv = lastValue; - lastValue = h; - return h !== '' && h !== lv && !set.blockedHosts?.includes(h); - }); - } if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } @@ -335,22 +293,6 @@ export default class extends Endpoint { // eslint- set.hcaptchaSecretKey = ps.hcaptchaSecretKey; } - if (ps.enableMcaptcha !== undefined) { - set.enableMcaptcha = ps.enableMcaptcha; - } - - if (ps.mcaptchaSiteKey !== undefined) { - set.mcaptchaSitekey = ps.mcaptchaSiteKey; - } - - if (ps.mcaptchaInstanceUrl !== undefined) { - set.mcaptchaInstanceUrl = ps.mcaptchaInstanceUrl; - } - - if (ps.mcaptchaSecretKey !== undefined) { - set.mcaptchaSecretKey = ps.mcaptchaSecretKey; - } - if (ps.enableRecaptcha !== undefined) { set.enableRecaptcha = ps.enableRecaptcha; } @@ -407,6 +349,10 @@ export default class extends Endpoint { // eslint- set.langs = ps.langs.filter(Boolean); } + if (ps.summalyProxy !== undefined) { + set.summalyProxy = ps.summalyProxy; + } + if (ps.enableEmail !== undefined) { set.enableEmail = ps.enableEmail; } @@ -452,7 +398,7 @@ export default class extends Endpoint { // eslint- } if (ps.repositoryUrl !== undefined) { - set.repositoryUrl = URL.canParse(ps.repositoryUrl!) ? ps.repositoryUrl : null; + set.repositoryUrl = ps.repositoryUrl; } if (ps.feedbackUrl !== undefined) { @@ -467,14 +413,6 @@ export default class extends Endpoint { // eslint- set.privacyPolicyUrl = ps.privacyPolicyUrl; } - if (ps.statusUrl !== undefined) { - set.statusUrl = ps.statusUrl; - } - - if (ps.inquiryUrl !== undefined) { - set.inquiryUrl = ps.inquiryUrl; - } - if (ps.useObjectStorage !== undefined) { set.useObjectStorage = ps.useObjectStorage; } @@ -639,26 +577,6 @@ export default class extends Endpoint { // eslint- } } - if (ps.enableTruemailApi !== undefined) { - set.enableTruemailApi = ps.enableTruemailApi; - } - - if (ps.truemailInstance !== undefined) { - if (ps.truemailInstance === '') { - set.truemailInstance = null; - } else { - set.truemailInstance = ps.truemailInstance; - } - } - - if (ps.truemailAuthKey !== undefined) { - if (ps.truemailAuthKey === '') { - set.truemailAuthKey = null; - } else { - set.truemailAuthKey = ps.truemailAuthKey; - } - } - if (ps.enableChartsForRemoteUser !== undefined) { set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser; } @@ -719,36 +637,6 @@ export default class extends Endpoint { // eslint- set.bannedEmailDomains = ps.bannedEmailDomains; } - if (ps.urlPreviewEnabled !== undefined) { - set.urlPreviewEnabled = ps.urlPreviewEnabled; - } - - if (ps.urlPreviewTimeout !== undefined) { - set.urlPreviewTimeout = ps.urlPreviewTimeout; - } - - if (ps.urlPreviewMaximumContentLength !== undefined) { - set.urlPreviewMaximumContentLength = ps.urlPreviewMaximumContentLength; - } - - if (ps.urlPreviewRequireContentLength !== undefined) { - set.urlPreviewRequireContentLength = ps.urlPreviewRequireContentLength; - } - - if (ps.urlPreviewUserAgent !== undefined) { - const value = (ps.urlPreviewUserAgent ?? '').trim(); - set.urlPreviewUserAgent = value === '' ? null : ps.urlPreviewUserAgent; - } - - if (ps.summalyProxy !== undefined || ps.urlPreviewSummaryProxyUrl !== undefined) { - const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim(); - set.urlPreviewSummaryProxyUrl = value === '' ? null : value; - } - - if (ps.urlPreviewDirectSummalyProxy !== undefined) { - set.directSummalyProxy = ps.urlPreviewDirectSummalyProxy; - } - if (ps.doNotSendNotificationEmailsForAbuseReport !== undefined) { set.doNotSendNotificationEmailsForAbuseReport = ps.doNotSendNotificationEmailsForAbuseReport; } diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index e9930422c0..38ca838a8c 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index ff8dd73605..450169dae6 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; -import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; +import { AnnouncementService } from '@/core/AnnouncementService.js'; import { DI } from '@/di-symbols.js'; -import type { AnnouncementsRepository } from '@/models/_.js'; +import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js'; export const meta = { tags: ['meta'], @@ -44,8 +44,11 @@ export default class extends Endpoint { // eslint- @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, + private queryService: QueryService, - private announcementEntityService: AnnouncementEntityService, + private announcementService: AnnouncementService, ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId) @@ -57,7 +60,7 @@ export default class extends Endpoint { // eslint- const announcements = await query.limit(ps.limit).getMany(); - return this.announcementEntityService.packMany(announcements, me); + return this.announcementService.packMany(announcements, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/announcements/show.ts b/packages/backend/src/server/api/endpoints/announcements/show.ts deleted file mode 100644 index 6312a0a54c..0000000000 --- a/packages/backend/src/server/api/endpoints/announcements/show.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import { EntityNotFoundError } from 'typeorm'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: false, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Announcement', - }, - - errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: 'b57b5e1d-4f49-404a-9edb-46b00268f121', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - announcementId: { type: 'string', format: 'misskey:id' }, - }, - required: ['announcementId'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private announcementService: AnnouncementService, - ) { - super(meta, paramDef, async (ps, me) => { - try { - return await this.announcementService.getAnnouncement(ps.announcementId, me); - } catch (err) { - if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement); - throw err; - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 16ffc6101b..cab9b8b47a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -71,11 +71,11 @@ export const paramDef = { } }, caseSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, - excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, + notify: { type: 'boolean' }, }, - required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], + required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], } as const; @Injectable() @@ -103,7 +103,7 @@ export default class extends Endpoint { // eslint- const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id, }); - if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) { + if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } @@ -132,7 +132,7 @@ export default class extends Endpoint { // eslint- const now = new Date(); - const antenna = await this.antennasRepository.insertOne({ + const antenna = await this.antennasRepository.insert({ id: this.idService.gen(now.getTime()), lastUsedAt: now, userId: me.id, @@ -145,10 +145,10 @@ export default class extends Endpoint { // eslint- users: ps.users, caseSensitive: ps.caseSensitive, localOnly: ps.localOnly, - excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - }); + notify: ps.notify, + }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('antennaCreated', antenna); diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index 2258954b56..986d611924 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts index 83d29f9c8c..abab2bc986 100644 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ b/packages/backend/src/server/api/endpoints/antennas/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 87127d80b4..4ff5c65192 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,7 +14,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -93,7 +92,7 @@ export default class extends Endpoint { // eslint- antenna.isActive = true; antenna.lastUsedAt = new Date(); - trackPromise(this.antennasRepository.update(antenna.id, antenna)); + this.antennasRepository.update(antenna.id, antenna); if (needPublishEvent) { this.globalEventService.publishInternalEvent('antennaUpdated', antenna); @@ -111,8 +110,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('user.isIndexable = true'); + .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); @@ -125,7 +123,9 @@ export default class extends Endpoint { // eslint- notes.sort((a, b) => a.id > b.id ? -1 : 1); } - this.noteReadService.read(me.id, notes); + if (notes.length > 0) { + this.noteReadService.read(me.id, notes); + } return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts index a40f187d0b..22fa41c22c 100644 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ b/packages/backend/src/server/api/endpoints/antennas/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index be7b9c2328..718f934494 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -70,11 +70,11 @@ export const paramDef = { } }, caseSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, - excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, + notify: { type: 'boolean' }, }, - required: ['antennaId'], + required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], } as const; @Injectable() @@ -93,10 +93,8 @@ export default class extends Endpoint { // eslint- private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.keywords && ps.excludeKeywords) { - if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new Error('either keywords or excludeKeywords is required.'); - } + if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { + throw new Error('either keywords or excludeKeywords is required.'); } // Fetch the antenna const antenna = await this.antennasRepository.findOneBy({ @@ -111,7 +109,7 @@ export default class extends Endpoint { // eslint- let userList; let userGroupJoining; - if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) { + if (ps.src === 'list' && ps.userListId) { userList = await this.userListsRepository.findOneBy({ id: ps.userListId, userId: me.id, @@ -134,16 +132,16 @@ export default class extends Endpoint { // eslint- await this.antennasRepository.update(antenna.id, { name: ps.name, src: ps.src, - userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined, + userListId: userList ? userList.id : null, userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, keywords: ps.keywords, excludeKeywords: ps.excludeKeywords, users: ps.users, caseSensitive: ps.caseSensitive, localOnly: ps.localOnly, - excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, + notify: ps.notify, isActive: true, lastUsedAt: new Date(), }); diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index d8c55de7ec..42ddf90c22 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index d3c40dba59..43737bcb26 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -148,7 +148,7 @@ export default class extends Endpoint { // eslint- if (user != null) { return { type: 'User', - object: await this.userEntityService.pack(user, me, { schema: 'UserDetailedNotMe' }), + object: await this.userEntityService.pack(user, me, { detail: true }), }; } else if (note != null) { try { diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index ba847fc4f0..42174c8400 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -54,7 +54,7 @@ export default class extends Endpoint { // eslint- const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1'))); // Create account - const app = await this.appsRepository.insertOne({ + const app = await this.appsRepository.insert({ id: this.idService.gen(), userId: me ? me.id : null, name: ps.name, @@ -62,7 +62,7 @@ export default class extends Endpoint { // eslint- permission, callbackUrl: ps.callbackUrl, secret: secret, - }); + }).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0])); return await this.appEntityService.pack(app, null, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index ad4261440e..1b13a36b2f 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 2e62f04df0..6b466109fc 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -62,7 +62,7 @@ export default class extends Endpoint { // eslint- const accessToken = secureRndstr(32); // Fetch exist access token - const exist = await this.accessTokensRepository.exists({ + const exist = await this.accessTokensRepository.exist({ where: { appId: session.appId, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index f8ddfdb75c..b14e75c4a7 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -78,11 +78,11 @@ export default class extends Endpoint { // eslint- const token = randomUUID(); // Create session token document - const doc = await this.authSessionsRepository.insertOne({ + const doc = await this.authSessionsRepository.insert({ id: this.idService.gen(), appId: app.id, token: token, - }); + }).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0])); return { token: doc.token, diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts index 13e02a2541..b013685fd3 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts index b490c5832d..28470362c9 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -112,7 +112,7 @@ export default class extends Endpoint { // eslint- return { accessToken: accessToken.token, user: await this.userEntityService.pack(session.userId, null, { - schema: 'UserDetailedNotMe', + detail: true, }), }; }); diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 7faba07e41..099b9cb8d7 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -1,15 +1,14 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository, MutingsRepository } from '@/models/_.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; -import { UserMutingService } from '@/core/UserMutingService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; @@ -70,13 +69,9 @@ export default class extends Endpoint { // eslint- @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - private userEntityService: UserEntityService, private getterService: GetterService, private userBlockingService: UserBlockingService, - private userMutingService: UserMutingService, ) { super(meta, paramDef, async (ps, me) => { const blocker = await this.usersRepository.findOneByOrFail({ id: me.id }); @@ -93,7 +88,7 @@ export default class extends Endpoint { // eslint- }); // Check if already blocking - const exist = await this.blockingsRepository.exists({ + const exist = await this.blockingsRepository.exist({ where: { blockerId: blocker.id, blockeeId: blockee.id, @@ -104,22 +99,10 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.alreadyBlocking); } - await Promise.all([ - this.userBlockingService.block(blocker, blockee), - this.mutingsRepository.exists({ - where: { - muteeId: blockee.id, - muterId: blocker.id, - }, - }).then(exists => { - if (!exists) { - this.userMutingService.mute(blocker, blockee, null); - } - }), - ]); + await this.userBlockingService.block(blocker, blockee); return await this.userEntityService.pack(blockee.id, blocker, { - schema: 'UserDetailedNotMe', + detail: true, }); }); } diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index cebb307338..6eb43891aa 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -88,7 +88,7 @@ export default class extends Endpoint { // eslint- }); // Check not blocking - const exist = await this.blockingsRepository.exists({ + const exist = await this.blockingsRepository.exist({ where: { blockerId: blocker.id, blockeeId: blockee.id, @@ -103,7 +103,7 @@ export default class extends Endpoint { // eslint- await this.userBlockingService.unblock(blocker, blockee); return await this.userEntityService.pack(blockee.id, blocker, { - schema: 'UserDetailedNotMe', + detail: true, }); }); } diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 8431fa6b34..58fd5140a0 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts deleted file mode 100644 index ab877bbe20..0000000000 --- a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { BubbleGameRecordsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - -export const meta = { - allowGet: true, - cacheSec: 60, - - errors: { - }, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', format: 'misskey:id', - optional: false, nullable: false, - }, - score: { - type: 'integer', - optional: false, nullable: false, - }, - user: { - type: 'object', - optional: true, nullable: false, - ref: 'UserLite', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - gameMode: { type: 'string' }, - }, - required: ['gameMode'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.bubbleGameRecordsRepository) - private bubbleGameRecordsRepository: BubbleGameRecordsRepository, - - private userEntityService: UserEntityService, - ) { - super(meta, paramDef, async (ps) => { - const records = await this.bubbleGameRecordsRepository.find({ - where: { - gameMode: ps.gameMode, - seededAt: MoreThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)), - }, - order: { - score: 'DESC', - }, - take: 10, - relations: ['user'], - }); - - const users = await this.userEntityService.packMany(records.map(r => r.user!), null); - - return records.map(r => ({ - id: r.id, - score: r.score, - user: users.find(u => u.id === r.user!.id), - })); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/bubble-game/register.ts b/packages/backend/src/server/api/endpoints/bubble-game/register.ts deleted file mode 100644 index 0a999e42cd..0000000000 --- a/packages/backend/src/server/api/endpoints/bubble-game/register.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { IdService } from '@/core/IdService.js'; -import type { BubbleGameRecordsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - requireCredential: true, - - kind: 'write:account', - - limit: { - duration: ms('1hour'), - max: 120, - minInterval: ms('30sec'), - }, - - errors: { - invalidSeed: { - message: 'Provided seed is invalid.', - code: 'INVALID_SEED', - id: 'eb627bc7-574b-4a52-a860-3c3eae772b88', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - score: { type: 'integer', minimum: 0 }, - seed: { type: 'string', minLength: 1, maxLength: 1024 }, - logs: { - type: 'array', - items: { - type: 'array', - items: { - type: 'number', - }, - }, - }, - gameMode: { type: 'string' }, - gameVersion: { type: 'integer' }, - }, - required: ['score', 'seed', 'logs', 'gameMode', 'gameVersion'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.bubbleGameRecordsRepository) - private bubbleGameRecordsRepository: BubbleGameRecordsRepository, - - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - const seedDate = new Date(parseInt(ps.seed, 10)); - const now = new Date(); - - // シードが未来なのは通常のプレイではありえないので弾く - if (seedDate.getTime() > now.getTime()) { - throw new ApiError(meta.errors.invalidSeed); - } - - // シードが古すぎる(5時間以上前)のも弾く - if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60 * 5) { - throw new ApiError(meta.errors.invalidSeed); - } - - await this.bubbleGameRecordsRepository.insert({ - id: this.idService.gen(now.getTime()), - seed: ps.seed, - seededAt: seedDate, - userId: me.id, - score: ps.score, - logs: ps.logs, - gameMode: ps.gameMode, - gameVersion: ps.gameVersion, - isVerified: false, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index e3a6d2d670..520577c831 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -80,7 +80,7 @@ export default class extends Endpoint { // eslint- } } - const channel = await this.channelsRepository.insertOne({ + const channel = await this.channelsRepository.insert({ id: this.idService.gen(), userId: me.id, name: ps.name, @@ -89,7 +89,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive ?? false, ...(ps.color !== undefined ? { color: ps.color } : {}), allowRenoteToExternal: ps.allowRenoteToExternal ?? true, - } as MiChannel); + } as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); return await this.channelEntityService.pack(channel, me); }); diff --git a/packages/backend/src/server/api/endpoints/channels/favorite.ts b/packages/backend/src/server/api/endpoints/channels/favorite.ts index a1ae9b80a7..e7564d7708 100644 --- a/packages/backend/src/server/api/endpoints/channels/favorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/favorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts index a9a79ba8fc..22170d8008 100644 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ b/packages/backend/src/server/api/endpoints/channels/featured.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 1812820ba2..3a38d1aa7b 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index d2f36f251e..64ecbcedc5 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts index d96e6c3ad2..e119ce9b4d 100644 --- a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts index daab685f1b..22016c4615 100644 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ b/packages/backend/src/server/api/endpoints/channels/owned.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index ae32203603..fba8e7ea68 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts index 332ce2c9dc..02d806f90c 100644 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ b/packages/backend/src/server/api/endpoints/channels/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index a2de0d570b..d56cd0f717 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -102,7 +102,6 @@ export default class extends Endpoint { // eslint- redisTimelines: [`channelTimeline:${channel.id}`], excludePureRenotes: false, withCats: false, - withoutBots: false, dbFallback: async (untilId, sinceId, limit) => { return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); }, diff --git a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts index fc6b75e295..a7f03ec181 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index 48c5261135..b93219ff38 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index dba2938b39..6dcade4b17 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts index fd21e3d9fe..54ed61936e 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts index cbe792376b..7f3561076c 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index d32bc765a4..5ed2719647 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index dad21e9e8e..89cafee68f 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index 68aa12ac0e..9af91729f7 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index e1979cfe8b..fef17b6a8f 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index dcb72084b7..77e5d3d1bf 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 0a019ce4fb..eb56651b5d 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index 06b15bca18..6614856639 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/user/pv.ts b/packages/backend/src/server/api/endpoints/charts/user/pv.ts index d359b491e2..961fd48613 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index 4355aa5348..b24fb40db5 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index 1f5f5fea54..19a5dbe74c 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index d7c9ea3964..58037a8fa5 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index ceebc8ba5e..8abfe0925d 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts index ca8ff2e1f1..006d485abb 100644 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ b/packages/backend/src/server/api/endpoints/clips/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts index 11f8ec3e92..51f013e634 100644 --- a/packages/backend/src/server/api/endpoints/clips/favorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -62,7 +62,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchClip); } - const exist = await this.clipFavoritesRepository.exists({ + const exist = await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index 2e4a3ff820..ede0a7a182 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts index 44719592d1..894c7150ad 100644 --- a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 943c31c894..d6029a9145 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 33f9ecd25b..cc7ef43723 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index 1078a1b176..3ccd17d238 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts index a458fda4a0..d537a25f3d 100644 --- a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 603a3ccf3d..65581cc378 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * 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 { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipService } from '@/core/ClipService.js'; @@ -41,7 +41,7 @@ export const paramDef = { isPublic: { type: 'boolean' }, description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, }, - required: ['clipId'], + required: ['clipId', 'name'], } as const; @Injectable() diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 7e9b0fa0e1..2a2be9acca 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index 10c521332d..8cbae234d7 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -36,7 +36,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, 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] }, + sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 4670392025..1f843a0ec1 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -74,7 +74,7 @@ export default class extends Endpoint { // eslint- } const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); - query.andWhere(':file <@ note.fileIds', { file: [file.id] }); + query.andWhere(':file = ANY(note.fileIds)', { file: file.id }); const notes = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index cc7920505f..ae45829fa2 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -38,7 +38,7 @@ export default class extends Endpoint { // eslint- private driveFilesRepository: DriveFilesRepository, ) { super(meta, paramDef, async (ps, me) => { - const exist = await this.driveFilesRepository.exists({ + const exist = await this.driveFilesRepository.exist({ where: { md5: ps.md5, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 50fd33c023..3aa1ad70a4 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -55,11 +55,6 @@ export const meta = { code: 'NO_FREE_SPACE', id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064', }, - invalidFileSize: { - message: 'File size exceeds limit.', - code: 'INVALID_FILE_SIZE', - id: '9068668f-0465-4c0e-8341-1c52fd6f5ab3', - }, }, } as const; @@ -119,7 +114,6 @@ export default class extends Endpoint { // eslint- if (err instanceof IdentifiableError) { if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate); if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace); - if (err.id === 'e5989b6d-ae66-49ed-88af-516ded10ca0c') throw new ApiError(meta.errors.invalidFileSize); } throw new ApiError(); } finally { diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index fa6e11da49..c4cdd6d6a4 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index 090cff6875..1a0b3640ea 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index 502d42f9e0..3bac681d8a 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -54,7 +54,7 @@ export default class extends Endpoint { // eslint- folderId: ps.folderId ?? IsNull(), }); - return await this.driveFileEntityService.packMany(files, { self: true }); + return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true }))); }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index e8f4539d61..5b5b459090 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 df1622cce0..7a70663dab 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index f047ca7f1c..3ee2fcd79f 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index 8c4848f8e1..228a4cf9f7 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index 08d9d9cdc3..293e96fe88 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -75,12 +75,12 @@ export default class extends Endpoint { // eslint- } // Create folder - const folder = await this.driveFoldersRepository.insertOne({ + const folder = await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: ps.name, parentId: parent !== null ? parent.id : null, userId: me.id, - }); + }).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0])); const folderObj = await this.driveFolderEntityService.pack(folder); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index 85d63873a4..7c5345443a 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts index eb45a30bc0..af1fd4132b 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts index a1c0df6697..73814034fb 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 62b04e1df3..32ef44e442 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -95,14 +95,15 @@ export default class extends Endpoint { // eslint- // Check if the circular reference will occur const checkCircle = async (folderId: string): Promise => { - const folder2 = await this.driveFoldersRepository.findOneByOrFail({ + // Fetch folder + const folder2 = await this.driveFoldersRepository.findOneBy({ id: folderId, }); - if (folder2.id === folder.id) { + if (folder2!.id === folder!.id) { return true; - } else if (folder2.parentId) { - return await checkCircle(folder2.parentId); + } else if (folder2!.parentId) { + return await checkCircle(folder2!.parentId); } else { return false; } diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts index f7c1ed39b5..3348375ffe 100644 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts index 1d7dacd60e..3889fe20ec 100644 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ b/packages/backend/src/server/api/endpoints/email-address/available.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/emoji.ts b/packages/backend/src/server/api/endpoints/emoji.ts index ccfbda0d44..81717c013b 100644 --- a/packages/backend/src/server/api/endpoints/emoji.ts +++ b/packages/backend/src/server/api/endpoints/emoji.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 46ef4eca1b..49ccf37152 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index fe7e9c36f3..2e2f1a09e6 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 4aedf62a84..5e52c5d624 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts index 5ff099524d..d68c22004e 100644 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index ce4dd13067..2b0025d7f5 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 1a793889c7..e241beb18d 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 310d80aaf6..43210932b9 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -60,7 +60,6 @@ export const paramDef = { '-firstRetrievedAt', '+latestRequestReceivedAt', '-latestRequestReceivedAt', - null, ], }, }, @@ -98,12 +97,6 @@ export default class extends Endpoint { // eslint- default: query.orderBy('instance.id', 'DESC'); break; } - if (me == null) { - ps.blocked = false; - ps.suspended = false; - ps.silenced = false; - } - if (typeof ps.blocked === 'boolean') { const meta = await this.metaService.fetch(true); if (ps.blocked) { @@ -123,9 +116,9 @@ export default class extends Endpoint { // eslint- if (typeof ps.suspended === 'boolean') { if (ps.suspended) { - query.andWhere('instance.suspensionState != \'none\''); + query.andWhere('instance.isSuspended = TRUE'); } else { - query.andWhere('instance.suspensionState = \'none\''); + query.andWhere('instance.isSuspended = FALSE'); } } diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 2972861a4b..91b1789b25 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -43,7 +43,7 @@ export default class extends Endpoint { // eslint- const instance = await this.instancesRepository .findOneBy({ host: this.utilityService.toPuny(ps.host) }); - return instance ? await this.instanceEntityService.pack(instance, me) : null; + return instance ? await this.instanceEntityService.pack(instance) : null; }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index bac54970ab..9e2084b3e8 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts index f8430ef431..721dff0202 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index 71b1aeb07b..c25b9b1521 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -54,7 +54,7 @@ export default class extends Endpoint { // eslint- .limit(ps.limit) .getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailedNotMe' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts index f36136d53b..34e576e2af 100644 --- a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts +++ b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index ba48b0119e..f7f40f0216 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -20,185 +20,10 @@ export const meta = { res: { type: 'object', properties: { - image: { - type: 'object', - optional: true, - properties: { - link: { - type: 'string', - optional: true, - }, - url: { - type: 'string', - optional: false, - }, - title: { - type: 'string', - optional: true, - }, - }, - }, - paginationLinks: { - type: 'object', - optional: true, - properties: { - self: { - type: 'string', - optional: true, - }, - first: { - type: 'string', - optional: true, - }, - next: { - type: 'string', - optional: true, - }, - last: { - type: 'string', - optional: true, - }, - prev: { - type: 'string', - optional: true, - }, - }, - }, - link: { - type: 'string', - optional: true, - }, - title: { - type: 'string', - optional: true, - }, items: { type: 'array', - optional: false, items: { type: 'object', - properties: { - link: { - type: 'string', - optional: true, - }, - guid: { - type: 'string', - optional: true, - }, - title: { - type: 'string', - optional: true, - }, - pubDate: { - type: 'string', - optional: true, - }, - creator: { - type: 'string', - optional: true, - }, - summary: { - type: 'string', - optional: true, - }, - content: { - type: 'string', - optional: true, - }, - isoDate: { - type: 'string', - optional: true, - }, - categories: { - type: 'array', - optional: true, - items: { - type: 'string', - }, - }, - contentSnippet: { - type: 'string', - optional: true, - }, - enclosure: { - type: 'object', - optional: true, - properties: { - url: { - type: 'string', - optional: false, - }, - length: { - type: 'number', - optional: true, - }, - type: { - type: 'string', - optional: true, - }, - }, - }, - }, - }, - }, - feedUrl: { - type: 'string', - optional: true, - }, - description: { - type: 'string', - optional: true, - }, - itunes: { - type: 'object', - optional: true, - additionalProperties: true, - properties: { - image: { - type: 'string', - optional: true, - }, - owner: { - type: 'object', - optional: true, - properties: { - name: { - type: 'string', - optional: true, - }, - email: { - type: 'string', - optional: true, - }, - }, - }, - author: { - type: 'string', - optional: true, - }, - summary: { - type: 'string', - optional: true, - }, - explicit: { - type: 'string', - optional: true, - }, - categories: { - type: 'array', - optional: true, - items: { - type: 'string', - }, - }, - keywords: { - type: 'array', - optional: true, - items: { - type: 'string', - }, - }, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 64f13a577e..df194df34e 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -44,7 +44,6 @@ export const paramDef = { permissions: { type: 'array', items: { type: 'string', } }, - visibility: { type: 'string', enum: ['public', 'private'], default: 'public' }, }, required: ['title', 'summary', 'script', 'permissions'], } as const; @@ -59,7 +58,7 @@ export default class extends Endpoint { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const flash = await this.flashsRepository.insertOne({ + const flash = await this.flashsRepository.insert({ id: this.idService.gen(), userId: me.id, updatedAt: new Date(), @@ -67,8 +66,7 @@ export default class extends Endpoint { // eslint- summary: ps.summary, script: ps.script, permissions: ps.permissions, - visibility: ps.visibility, - }); + }).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0])); return await this.flashEntityService.pack(flash); }); diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts index d3d47e5deb..b128010d0f 100644 --- a/packages/backend/src/server/api/endpoints/flash/delete.ts +++ b/packages/backend/src/server/api/endpoints/flash/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts index c2d6ab5085..f05850e490 100644 --- a/packages/backend/src/server/api/endpoints/flash/featured.ts +++ b/packages/backend/src/server/api/endpoints/flash/featured.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/flash/gen-token.ts b/packages/backend/src/server/api/endpoints/flash/gen-token.ts index 959bea0c4d..fbfe912966 100644 --- a/packages/backend/src/server/api/endpoints/flash/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/flash/gen-token.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts index e4dc5b61c5..66fa22639f 100644 --- a/packages/backend/src/server/api/endpoints/flash/like.ts +++ b/packages/backend/src/server/api/endpoints/flash/like.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -70,7 +70,7 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.flashLikesRepository.exists({ + const exist = await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/flash/my-likes.ts b/packages/backend/src/server/api/endpoints/flash/my-likes.ts index 755cc5acfc..f24af7b7bc 100644 --- a/packages/backend/src/server/api/endpoints/flash/my-likes.ts +++ b/packages/backend/src/server/api/endpoints/flash/my-likes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/flash/my.ts b/packages/backend/src/server/api/endpoints/flash/my.ts index 5746096232..573fd26522 100644 --- a/packages/backend/src/server/api/endpoints/flash/my.ts +++ b/packages/backend/src/server/api/endpoints/flash/my.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/flash/show.ts b/packages/backend/src/server/api/endpoints/flash/show.ts index a6fbd8e76e..f98aa194ec 100644 --- a/packages/backend/src/server/api/endpoints/flash/show.ts +++ b/packages/backend/src/server/api/endpoints/flash/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/flash/unlike.ts b/packages/backend/src/server/api/endpoints/flash/unlike.ts index 7869bcdf52..06e000d4fb 100644 --- a/packages/backend/src/server/api/endpoints/flash/unlike.ts +++ b/packages/backend/src/server/api/endpoints/flash/unlike.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts index 8696c6f6e8..149ccb9ffd 100644 --- a/packages/backend/src/server/api/endpoints/flash/update.ts +++ b/packages/backend/src/server/api/endpoints/flash/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -51,7 +51,7 @@ export const paramDef = { } }, visibility: { type: 'string', enum: ['public', 'private'] }, }, - required: ['flashId'], + required: ['flashId', 'title', 'summary', 'script', 'permissions'], } as const; @Injectable() @@ -71,11 +71,11 @@ export default class extends Endpoint { // eslint- await this.flashsRepository.update(flash.id, { updatedAt: new Date(), - ...Object.fromEntries( - Object.entries(ps).filter( - ([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key), - ), - ), + title: ps.title, + summary: ps.summary, + script: ps.script, + permissions: ps.permissions, + visibility: ps.visibility, }); }); } diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index db320e7129..194d13cfe1 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -100,11 +100,22 @@ export default class extends Endpoint { // eslint- throw err; }); + // Check if already following + const exist = await this.followingsRepository.exist({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyFollowing); + } + try { await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies }); } catch (e) { if (e instanceof IdentifiableError) { - if (e.id === 'ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced') throw new ApiError(meta.errors.alreadyFollowing); if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked); } diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index ba146b6703..c7e677be99 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -85,7 +85,7 @@ export default class extends Endpoint { // eslint- }); // Check not following - const exist = await this.followingsRepository.exists({ + const exist = await this.followingsRepository.exist({ where: { followerId: follower.id, followeeId: followee.id, diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 8935c2c2da..028b1c47bc 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index 2d1446681c..bbb0b518ba 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 6d663d480c..ebba53e96d 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index fa59e38976..7160009d69 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -71,7 +71,7 @@ export default class extends Endpoint { // eslint- .limit(ps.limit) .getMany(); - return await this.followRequestEntityService.packMany(requests, me); + return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req))); }); } } diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index 4f78eae677..ecb2570cc1 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/following/update-all.ts b/packages/backend/src/server/api/endpoints/following/update-all.ts index c953feb393..5859c4c29e 100644 --- a/packages/backend/src/server/api/endpoints/following/update-all.ts +++ b/packages/backend/src/server/api/endpoints/following/update-all.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/following/update.ts b/packages/backend/src/server/api/endpoints/following/update.ts index d62cf210ed..2a0429de64 100644 --- a/packages/backend/src/server/api/endpoints/following/update.ts +++ b/packages/backend/src/server/api/endpoints/following/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index 7d2878e03f..3ac29fc1ad 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts index 4ee252104a..d95e8469a3 100644 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index d398418ab4..cd8c170a97 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 504a9c789e..50e08ee42d 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -69,13 +69,13 @@ export default class extends Endpoint { // eslint- id: fileId, userId: me.id, }), - ))).filter(x => x != null); + ))).filter((file): file is MiDriveFile => file != null); if (files.length === 0) { throw new Error(); } - const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({ + const post = await this.galleryPostsRepository.insert(new MiGalleryPost({ id: this.idService.gen(), updatedAt: new Date(), title: ps.title, @@ -83,7 +83,7 @@ export default class extends Endpoint { // eslint- userId: me.id, isSensitive: ps.isSensitive, fileIds: files.map(file => file.id), - })); + })).then(x => this.galleryPostsRepository.findOneByOrFail(x.identifiers[0])); return await this.galleryPostEntityService.pack(post, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index 527e3fb52d..994e5606e4 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 91e49e6463..c825e20ec2 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -72,7 +72,7 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.galleryLikesRepository.exists({ + const exist = await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts index bd69898229..a9483d4e56 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index f44e2c7afc..07ae594888 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 5243ee9603..878a05384c 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -47,7 +47,7 @@ export const paramDef = { } }, isSensitive: { type: 'boolean', default: false }, }, - required: ['postId'], + required: ['postId', 'title', 'fileIds'], } as const; @Injectable() @@ -62,19 +62,15 @@ export default class extends Endpoint { // eslint- private galleryPostEntityService: GalleryPostEntityService, ) { super(meta, paramDef, async (ps, me) => { - let files: Array | undefined; - - if (ps.fileIds) { - files = (await Promise.all(ps.fileIds.map(fileId => - this.driveFilesRepository.findOneBy({ - id: fileId, - userId: me.id, - }), - ))).filter(x => x != null); - - if (files.length === 0) { - throw new Error(); - } + const files = (await Promise.all(ps.fileIds.map(fileId => + this.driveFilesRepository.findOneBy({ + id: fileId, + userId: me.id, + }), + ))).filter((file): file is MiDriveFile => file != null); + + if (files.length === 0) { + throw new Error(); } await this.galleryPostsRepository.update({ @@ -85,7 +81,7 @@ export default class extends Endpoint { // eslint- title: ps.title, description: ps.description, isSensitive: ps.isSensitive, - fileIds: files ? files.map(file => file.id) : undefined, + fileIds: files.map(file => file.id), }); const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId }); diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts index 52acee1cfb..be7dec03d4 100644 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index a57774be73..b029c0739c 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index 5cd3c6584d..90fa9a699f 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index d4eb851054..0c2cb0bfbb 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -43,7 +43,7 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) - .orderBy('tag.mentionedLocalUsersCount', 'DESC') + .orderBy('tag.count', 'DESC') .groupBy('tag.id') .limit(ps.limit) .offset(ps.offset) diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index 940e3bd69d..6aeecb6fe3 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index cb8065e3a6..2a20986bf0 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index 8534289a68..6f7507fb12 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -1,12 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick 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 { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -48,9 +47,8 @@ export default class extends Endpoint { // eslint- private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); const query = this.usersRepository.createQueryBuilder('user') - .where(':tag <@ user.tags', { tag: [normalizeForSearch(ps.tag)] }) + .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) }) .andWhere('user.isSuspended = FALSE'); const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)); @@ -76,7 +74,7 @@ export default class extends Endpoint { // eslint- const users = await query.limit(ps.limit).getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index c24a033039..8876d1c382 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -71,8 +71,8 @@ export default class extends Endpoint { // eslint- userProfile.loggedInDates = [...userProfile.loggedInDates, today]; } - return await this.userEntityService.pack(userProfile.user!, userProfile.user!, { - schema: 'MeDetailed', + return await this.userEntityService.pack(userProfile.user!, userProfile.user!, { + detail: true, includeSecrets: isSecure, userProfile, }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 2a30e8b0c3..543a2c93ce 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,19 +15,6 @@ export const meta = { requireCredential: true, secure: true, - - res: { - type: 'object', - properties: { - backupCodes: { - type: 'array', - optional: false, - items: { - type: 'string', - }, - }, - }, - }, } as const; export const paramDef = { @@ -77,7 +64,7 @@ export default class extends Endpoint { // eslint- // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: 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 b03a3d0d54..e6a6a9de71 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -86,7 +86,7 @@ export default class extends Endpoint { } } - const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } @@ -96,10 +96,10 @@ export default class extends Endpoint { } const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential); - const keyId = keyInfo.credentialID; + const credentialId = Buffer.from(keyInfo.credentialID).toString('base64url'); await this.userSecurityKeysRepository.insert({ - id: keyId, + id: credentialId, userId: me.id, name: ps.name, publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'), @@ -111,12 +111,12 @@ export default class extends Endpoint { // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); return { - id: keyId, + id: credentialId, name: ps.name, }; }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts index bf039ccd16..2ff51fcc1c 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -74,7 +74,7 @@ export default class extends Endpoint { // eslint- // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: 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 ad3f3351e4..a851be60a6 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 @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository } from '@/models/_.js'; @@ -47,7 +47,7 @@ export const meta = { properties: { id: { type: 'string', - optional: true, + nullable: true, }, }, }, @@ -148,7 +148,6 @@ export const meta = { 'enterprise', 'indirect', 'none', - null, ], }, extensions: { @@ -217,7 +216,7 @@ export default class extends Endpoint { } } - const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } 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 1d71cc7434..8180bc38c4 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import * as OTPAuth from 'otpauth'; import * as QRCode from 'qrcode'; import { Inject, Injectable } from '@nestjs/common'; @@ -77,7 +77,7 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } 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 e830fffa94..caf62f169f 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 @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; @@ -66,7 +66,7 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } @@ -97,7 +97,7 @@ export default class extends Endpoint { // eslint- // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: 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 900aeb55bc..032c316de1 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -62,7 +62,7 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await comparePassword(ps.password, profile.password ?? ''); + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } @@ -76,7 +76,7 @@ export default class extends Endpoint { // eslint- // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: 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 cfa07cc8d7..c8625d0405 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -69,7 +69,7 @@ export default class extends Endpoint { // eslint- // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, })); diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 91c8597b1b..aca50639f6 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -21,26 +21,21 @@ export const meta = { properties: { id: { type: 'string', - optional: false, format: 'misskey:id', }, name: { type: 'string', - optional: true, }, createdAt: { type: 'string', - optional: false, format: 'date-time', }, lastUsedAt: { type: 'string', - optional: true, format: 'date-time', }, permission: { type: 'array', - optional: false, uniqueItems: true, items: { type: 'string', diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 0b4faf5ef8..5e6d620bc7 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -23,19 +23,16 @@ export const meta = { id: { type: 'string', format: 'misskey:id', - optional: false, }, name: { type: 'string', - optional: false, }, callbackUrl: { type: 'string', - optional: false, nullable: true, + nullable: true, }, permission: { type: 'array', - optional: false, uniqueItems: true, items: { type: 'string', @@ -43,7 +40,6 @@ export const meta = { }, isAuthorized: { type: 'boolean', - optional: true, }, }, }, 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 4dca633610..61afae98bc 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { hashPassword, comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository } from '@/models/_.js'; @@ -50,14 +50,15 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await comparePassword(ps.currentPassword, profile.password!); + const passwordMatched = await bcrypt.compare(ps.currentPassword, profile.password!); if (!passwordMatched) { throw new Error('incorrect password'); } // Generate hash of password - const hash = await hashPassword(ps.newPassword); + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(ps.newPassword, salt); await this.userProfilesRepository.update(me.id, { password: hash, 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 e70905ef1b..02af198d66 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 4da8920a66..e5f5e04112 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -59,7 +59,7 @@ export default class extends Endpoint { // eslint- return; } - const passwordMatched = await comparePassword(ps.password, profile.password!); + const passwordMatched = await bcrypt.compare(ps.password, profile.password!); if (!passwordMatched) { throw new Error('incorrect password'); } diff --git a/packages/backend/src/server/api/endpoints/i/export-antennas.ts b/packages/backend/src/server/api/endpoints/i/export-antennas.ts index 77fb4a895f..8d3dc4aca6 100644 --- a/packages/backend/src/server/api/endpoints/i/export-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/export-antennas.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts index 7573018bec..4f1713bcf9 100644 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/export-clips.ts b/packages/backend/src/server/api/endpoints/i/export-clips.ts deleted file mode 100644 index 10d1fdac73..0000000000 --- a/packages/backend/src/server/api/endpoints/i/export-clips.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueueService } from '@/core/QueueService.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: ms('1day'), - max: 1, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private queueService: QueueService, - ) { - super(meta, paramDef, async (ps, me) => { - this.queueService.createExportClipsJob(me); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/export-favorites.ts b/packages/backend/src/server/api/endpoints/i/export-favorites.ts index 5e03f70170..623646e937 100644 --- a/packages/backend/src/server/api/endpoints/i/export-favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/export-favorites.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts index 2e5ba14737..ea2b3505c0 100644 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ b/packages/backend/src/server/api/endpoints/i/export-following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts index 0384cf142b..f7429269df 100644 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts index db4e78f667..144c073c5d 100644 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts index 6cd662102c..21d725ed08 100644 --- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index 3558035eca..c4a3a9976a 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index d492585ffa..e8c70de06d 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index 73a6fcc98b..4adebf8781 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts index bc46163e3d..926e09c847 100644 --- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -71,14 +71,14 @@ export default class extends Endpoint { private downloadService: DownloadService, ) { super(meta, paramDef, async (ps, me) => { - const userExist = await this.usersRepository.exists({ where: { id: me.id } }); + const userExist = await this.usersRepository.exist({ where: { id: me.id } }); if (!userExist) throw new ApiError(meta.errors.noSuchUser); const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (file === null) throw new ApiError(meta.errors.noSuchFile); if (file.size === 0) throw new ApiError(meta.errors.emptyFile); const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url)); const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id }); - if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) { + if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } this.queueService.createImportAntennasJob(me, antennas); diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index 2606108539..1f67341b73 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -75,7 +75,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index d5e824df27..1df4aecd0d 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -75,7 +75,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index 0f5800404e..167d51a88c 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -75,7 +75,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index bacdd5c88f..08862b2a0f 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -74,7 +74,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 1ffa43282c..88a5d368a2 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -38,6 +38,11 @@ export const meta = { code: 'DESTINATION_ACCOUNT_FORBIDS', id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4', }, + rootForbidden: { + message: 'The root can\'t migrate.', + code: 'NOT_ROOT_FORBIDDEN', + id: '4362e8dc-731f-4ad8-a694-be2a88922a24', + }, noSuchUser: { message: 'No such user.', code: 'NO_SUCH_USER', @@ -86,6 +91,8 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { // check parameter if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser); + // abort if user is the root + if (me.isRoot) throw new ApiError(meta.errors.rootForbidden); // abort if user has already moved if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved); diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts index dc6ffd3e02..23aec8cb6e 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -1,13 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { In } from 'typeorm'; +import { Brackets, In } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; -import { obsoleteNotificationTypes, groupedNotificationTypes, FilterUnionByProperty } from '@/types.js'; +import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; @@ -48,10 +48,10 @@ export const paramDef = { markAsRead: { type: 'boolean', default: true }, // 後方互換のため、廃止された通知タイプも受け付ける includeTypes: { type: 'array', items: { - type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes], + type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], } }, excludeTypes: { type: 'array', items: { - type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes], + type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], } }, }, required: [], @@ -79,12 +79,12 @@ export default class extends Endpoint { // eslint- return []; } // excludeTypes に全指定されている場合はクエリしない - if (groupedNotificationTypes.every(type => ps.excludeTypes?.includes(type))) { + if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { return []; } - const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][]; - const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][]; + const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; + const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const notificationsRes = await this.redisClient.xrevrange( @@ -162,6 +162,7 @@ export default class extends Endpoint { // eslint- } groupedNotifications = groupedNotifications.slice(0, ps.limit); + const noteIds = groupedNotifications .filter((notification): notification is FilterUnionByProperty => ['mention', 'reply', 'quote'].includes(notification.type)) .map(notification => notification.noteId!); diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 2f619380e9..9b5558b6d3 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,13 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { In } from 'typeorm'; +import { Brackets, In } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; -import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; @@ -84,51 +84,27 @@ export default class extends Endpoint { // eslint- const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; - let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null; - let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null; - - let notifications: MiNotification[]; - for (;;) { - let notificationsRes: [id: string, fields: string[]][]; - - // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照 - if (sinceTime && !untilTime) { - notificationsRes = await this.redisClient.xrange( - `notificationTimeline:${me.id}`, - '(' + sinceTime, - '+', - 'COUNT', ps.limit); - } else { - notificationsRes = await this.redisClient.xrevrange( - `notificationTimeline:${me.id}`, - untilTime ? '(' + untilTime : '+', - sinceTime ? '(' + sinceTime : '-', - 'COUNT', ps.limit); - } - - if (notificationsRes.length === 0) { - return []; - } - - notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[]; - - if (includeTypes && includeTypes.length > 0) { - notifications = notifications.filter(notification => includeTypes.includes(notification.type)); - } else if (excludeTypes && excludeTypes.length > 0) { - notifications = notifications.filter(notification => !excludeTypes.includes(notification.type)); - } - - if (notifications.length !== 0) { - // 通知が1件以上ある場合は返す - break; - } - - // フィルタしたことで通知が0件になった場合、次のページを取得する - if (ps.sinceId && !ps.untilId) { - sinceTime = notificationsRes[notificationsRes.length - 1][0]; - } else { - untilTime = notificationsRes[notificationsRes.length - 1][0]; - } + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const notificationsRes = await this.redisClient.xrevrange( + `notificationTimeline:${me.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-', + 'COUNT', limit); + + if (notificationsRes.length === 0) { + return []; + } + + let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[]; + + if (includeTypes && includeTypes.length > 0) { + notifications = notifications.filter(notification => includeTypes.includes(notification.type)); + } else if (excludeTypes && excludeTypes.length > 0) { + notifications = notifications.filter(notification => !excludeTypes.includes(notification.type)); + } + + if (notifications.length === 0) { + return []; } // Mark all as read diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index d4c09426a7..bf09b73282 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index 1b6359a633..59191a1fa1 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index b7cafd74df..3ea954187f 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -66,8 +66,8 @@ export default class extends Endpoint { // eslint- throw err; }); - return await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + return await this.userEntityService.pack(me.id, me, { + detail: true, }); }); } diff --git a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts index 510c14a9b7..ede0957234 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts index d1a8eccb1d..f4c655d9ad 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 4db1ca73c1..026e738fa4 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 938aa98a95..bce236ce73 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; @@ -43,7 +43,7 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); // Compare password - const same = await comparePassword(ps.password, profile.password!); + const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { throw new Error('incorrect password'); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index f1797cfde7..fb01e3b44e 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index d53c390460..7f311d0f12 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -22,15 +22,6 @@ export const meta = { res: { type: 'object', - properties: { - updatedAt: { - type: 'string', - optional: false, - }, - value: { - optional: false, - }, - }, }, } as const; @@ -59,7 +50,7 @@ export default class extends Endpoint { // eslint- } return { - updatedAt: item.updatedAt.toISOString(), + updatedAt: item.updatedAt, value: item.value, }; }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index 86b0ec4c16..5e5e2552dc 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index 7b8af79025..5732f391f9 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,9 +13,6 @@ export const meta = { res: { type: 'object', - additionalProperties: { - type: 'string', - }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 28f158c62d..cd0d9bde7e 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,13 +10,6 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, kind: 'read:account', - - res: { - type: 'array', - items: { - type: 'string', - }, - }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index cf965ba0cf..e31fc35c10 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts index b1a8f09d9e..5abc0b0549 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 8723035d84..baa4292080 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index c05ee93c6f..4517753491 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -34,7 +34,7 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { if (ps.tokenId) { - const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } }); + const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } }); if (tokenExist) { await this.accessTokensRepository.delete({ @@ -43,7 +43,7 @@ export default class extends Endpoint { // eslint- }); } } else if (ps.token) { - const tokenExist = await this.accessTokensRepository.exists({ where: { token: ps.token } }); + const tokenExist = await this.accessTokensRepository.exist({ where: { token: ps.token } }); if (tokenExist) { await this.accessTokensRepository.delete({ diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 76ad0bbe21..733a0ca136 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index 74825cf9f3..8e02cd2c74 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -51,8 +51,8 @@ export default class extends Endpoint { // eslint- throw err; }); - return await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + return await this.userEntityService.pack(me.id, me, { + detail: true, }); }); } diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 98b73b44c5..d342a2fa96 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import { comparePassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -15,7 +15,6 @@ import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { UserAuthService } from '@/core/UserAuthService.js'; -import { MetaService } from '@/core/MetaService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -40,17 +39,11 @@ export const meta = { code: 'UNAVAILABLE', id: 'a2defefb-f220-8849-0af6-17f816099323', }, - - emailRequired: { - message: 'Email address is required.', - code: 'EMAIL_REQUIRED', - id: '324c7a88-59f2-492f-903f-89134f93e47e', - }, }, res: { type: 'object', - ref: 'MeDetailed', + ref: 'UserDetailed', }, } as const; @@ -73,7 +66,6 @@ export default class extends Endpoint { // eslint- @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private metaService: MetaService, private userEntityService: UserEntityService, private emailService: EmailService, private userAuthService: UserAuthService, @@ -95,7 +87,7 @@ export default class extends Endpoint { // eslint- } } - const passwordMatched = await comparePassword(ps.password, profile.password!); + const passwordMatched = await bcrypt.compare(ps.password, profile.password!); if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } @@ -105,8 +97,6 @@ export default class extends Endpoint { // eslint- if (!res.available) { throw new ApiError(meta.errors.unavailable); } - } else if ((await this.metaService.fetch()).emailRequiredForSignup) { - throw new ApiError(meta.errors.emailRequired); } await this.userProfilesRepository.update(me.id, { @@ -116,7 +106,7 @@ export default class extends Endpoint { // eslint- }); const iObj = await this.userEntityService.pack(me.id, me, { - schema: 'MeDetailed', + detail: true, includeSecrets: true, }); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index a63b5dbb0a..08c119201d 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; -import { RolePolicies, RoleService } from '@/core/RoleService.js'; +import { RoleService } from '@/core/RoleService.js'; import { CacheService } from '@/core/CacheService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; @@ -33,7 +33,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { Config } from '@/config.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { notificationRecieveConfig } from '@/models/json-schema/user.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -138,7 +137,7 @@ export const paramDef = { birthday: { ...birthdaySchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, - avatarDecorations: { type: 'array', maxItems: 128, items: { + avatarDecorations: { type: 'array', maxItems: 16, items: { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, @@ -173,7 +172,6 @@ export const paramDef = { autoAcceptFollowed: { type: 'boolean' }, noCrawle: { type: 'boolean' }, preventAiLearning: { type: 'boolean' }, - isIndexable: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, @@ -188,27 +186,7 @@ export const paramDef = { mutedInstances: { type: 'array', items: { type: 'string', } }, - notificationRecieveConfig: { - type: 'object', - nullable: false, - properties: { - note: notificationRecieveConfig, - follow: notificationRecieveConfig, - mention: notificationRecieveConfig, - reply: notificationRecieveConfig, - renote: notificationRecieveConfig, - quote: notificationRecieveConfig, - reaction: notificationRecieveConfig, - pollEnded: notificationRecieveConfig, - receiveFollowRequest: notificationRecieveConfig, - followRequestAccepted: notificationRecieveConfig, - groupInvited: notificationRecieveConfig, - roleAssigned: notificationRecieveConfig, - achievementEarned: notificationRecieveConfig, - app: notificationRecieveConfig, - test: notificationRecieveConfig, - }, - }, + notificationRecieveConfig: { type: 'object' }, emailNotificationTypes: { type: 'array', items: { type: 'string', } }, @@ -260,16 +238,8 @@ export default class extends Endpoint { // eslint- const profileUpdates = {} as Partial; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - let policies: RolePolicies | null = null; - - if (ps.name !== undefined) { - if (ps.name === null) { - updates.name = null; - } else { - const trimmedName = ps.name.trim(); - updates.name = trimmedName === '' ? null : trimmedName; - } - } + + if (ps.name !== undefined) updates.name = ps.name; if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; @@ -301,16 +271,14 @@ export default class extends Endpoint { // eslint- } if (ps.mutedWords !== undefined) { - policies ??= await this.roleService.getUserPolicies(user.id); - checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit); + checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } if (ps.hardMutedWords !== undefined) { - policies ??= await this.roleService.getUserPolicies(user.id); - checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit); + checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); validateMuteWordRegex(ps.hardMutedWords); profileUpdates.hardMutedWords = ps.hardMutedWords; } @@ -325,25 +293,17 @@ export default class extends Endpoint { // eslint- if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; - if (typeof ps.isIndexable === 'boolean') { - updates.isIndexable = ps.isIndexable; - profileUpdates.isIndexable = ps.isIndexable; - }; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') { - policies ??= await this.roleService.getUserPolicies(user.id); - if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); + if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; } if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.avatarId) { - policies ??= await this.roleService.getUserPolicies(user.id); - if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); - const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); @@ -359,9 +319,6 @@ export default class extends Endpoint { // eslint- } if (ps.bannerId) { - policies ??= await this.roleService.getUserPolicies(user.id); - if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); - const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); @@ -377,15 +334,14 @@ export default class extends Endpoint { // eslint- } if (ps.avatarDecorations) { - policies ??= await this.roleService.getUserPolicies(user.id); const decorations = await this.avatarDecorationService.getAll(true); - const myRoles = await this.roleService.getUserRoles(user.id); + const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .map(d => d.id); - if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); + if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, @@ -484,9 +440,9 @@ export default class extends Endpoint { // eslint- this.hashtagService.updateUsertags(user, tags); //#endregion - if (Object.keys(updates).length > 0) { - await this.usersRepository.update(user.id, updates); - this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id }); + if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates); + if (Object.keys(updates).includes('alsoKnownAs')) { + this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates }); } await this.userProfilesRepository.update(user.id, { @@ -494,8 +450,8 @@ export default class extends Endpoint { // eslint- verifiedLinks: [], }); - const iObj = await this.userEntityService.pack(user.id, user, { - schema: 'MeDetailed', + const iObj = await this.userEntityService.pack(user.id, user, { + detail: true, includeSecrets: isSecure, }); @@ -526,32 +482,26 @@ export default class extends Endpoint { // eslint- private async verifyLink(url: string, user: MiLocalUser) { if (!safeForSql(url)) return; - try { - const html = await this.httpRequestService.getHtml(url); + const html = await this.httpRequestService.getHtml(url); - const { window } = new JSDOM(html); - const doc = window.document; + const { window } = new JSDOM(html); + const doc = window.document; - const myLink = `${this.config.url}/@${user.username}`; + const myLink = `${this.config.url}/@${user.username}`; - const aEls = Array.from(doc.getElementsByTagName('a')); - const linkEls = Array.from(doc.getElementsByTagName('link')); + const aEls = Array.from(doc.getElementsByTagName('a')); + const linkEls = Array.from(doc.getElementsByTagName('link')); - const includesMyLink = aEls.some(a => a.href === myLink); - const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink); - - if (includesMyLink || includesRelMeLinks) { - await this.userProfilesRepository.createQueryBuilder('profile').update() - .where('userId = :userId', { userId: user.id }) - .set({ - verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている - }) - .execute(); - } + const includesMyLink = aEls.some(a => a.href === myLink); + const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink); - window.close(); - } catch (err) { - // なにもしない + if (includesMyLink || includesRelMeLinks) { + await this.userProfilesRepository.createQueryBuilder('profile').update() + .where('userId = :userId', { userId: user.id }) + .set({ + verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている + }) + .execute(); } } } diff --git a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts index 414aff59be..0de6c2327a 100644 --- a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts +++ b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 9eb7f5b3a0..47e2a0a1d6 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -85,18 +85,18 @@ export default class extends Endpoint { // eslint- const currentWebhooksCount = await this.webhooksRepository.countBy({ userId: me.id, }); - if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) { + if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) { throw new ApiError(meta.errors.tooManyWebhooks); } - const webhook = await this.webhooksRepository.insertOne({ + const webhook = await this.webhooksRepository.insert({ id: this.idService.gen(), userId: me.id, name: ps.name, url: ps.url, secret: ps.secret, on: ps.on, - }); + }).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('webhookCreated', webhook); @@ -108,7 +108,7 @@ export default class extends Endpoint { // eslint- url: webhook.url, secret: webhook.secret, active: webhook.active, - latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, + latestSentAt: webhook.latestSentAt?.toISOString(), latestStatus: webhook.latestStatus, }; }); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 1b1ac00670..d368e7593e 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index fe07afb2d0..0772145447 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -73,7 +73,7 @@ export default class extends Endpoint { // eslint- url: webhook.url, secret: webhook.secret, active: webhook.active, - latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, + latestSentAt: webhook.latestSentAt?.toISOString(), latestStatus: webhook.latestStatus, } )); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 5ddb79caf2..a81212faa6 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -85,7 +85,7 @@ export default class extends Endpoint { // eslint- url: webhook.url, secret: webhook.secret, active: webhook.active, - latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, + latestSentAt: webhook.latestSentAt?.toISOString(), latestStatus: webhook.latestStatus, }; }); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 07a25bd82a..1cecc541be 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -34,13 +34,13 @@ export const paramDef = { webhookId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, url: { type: 'string', minLength: 1, maxLength: 1024 }, - secret: { type: 'string', nullable: true, maxLength: 1024 }, + secret: { type: 'string', maxLength: 1024, default: '' }, on: { type: 'array', items: { type: 'string', enum: webhookEventTypes, } }, active: { type: 'boolean' }, }, - required: ['webhookId'], + required: ['webhookId', 'name', 'url', 'on', 'active'], } as const; // TODO: ロジックをサービスに切り出す @@ -66,7 +66,7 @@ export default class extends Endpoint { // eslint- await this.webhooksRepository.update(webhook.id, { name: ps.name, url: ps.url, - secret: ps.secret === null ? '' : ps.secret, + secret: ps.secret, on: ps.on, active: ps.active, }); diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index a70b587da7..97cc260337 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -66,13 +66,13 @@ export default class extends Endpoint { // eslint- } } - const ticket = await this.registrationTicketsRepository.insertOne({ + const ticket = await this.registrationTicketsRepository.insert({ id: this.idService.gen(), createdBy: me, createdById: me.id, expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null, code: generateInviteCode(), - }); + }).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])); return await this.inviteCodeEntityService.pack(ticket, me); }); diff --git a/packages/backend/src/server/api/endpoints/invite/delete.ts b/packages/backend/src/server/api/endpoints/invite/delete.ts index e960ff9f4e..7780877b20 100644 --- a/packages/backend/src/server/api/endpoints/invite/delete.ts +++ b/packages/backend/src/server/api/endpoints/invite/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts index 2786bd98d5..28974585b0 100644 --- a/packages/backend/src/server/api/endpoints/invite/limit.ts +++ b/packages/backend/src/server/api/endpoints/invite/limit.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts index 23aefe83a2..2234937417 100644 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/messaging/history.ts b/packages/backend/src/server/api/endpoints/messaging/history.ts index cf8f037638..c314ba46c7 100644 --- a/packages/backend/src/server/api/endpoints/messaging/history.ts +++ b/packages/backend/src/server/api/endpoints/messaging/history.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts index a97e9c57ee..718cd65d23 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -128,7 +128,7 @@ export default class extends Endpoint { // eslint- } } - return Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { + return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { populateRecipient: false, }))); } else if (ps.groupId != null) { @@ -160,16 +160,13 @@ export default class extends Endpoint { // eslint- // Mark all as read if (ps.markAsRead) { - await this.messagingService.readGroupMessagingMessage(me.id, recipientGroup.id, messages.map(x => x.id)); + this.messagingService.readGroupMessagingMessage(me.id, recipientGroup.id, messages.map(x => x.id)); } - return Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { + return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { populateGroup: false, }))); } - - // 必要に応じて適切な戻り値を提供する - return []; // デフォルトの戻り値を返す }); } } diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts index 173497abfc..46268f86b2 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts index c780684637..aeebe1202b 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts index e7c1849a52..3a8fc15965 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 5460635e1d..84ac0d4cf2 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,11 +1,19 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import JSON5 from 'json5'; +import type { AdsRepository, UsersRepository } from '@/models/_.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaEntityService } from '@/core/entities/MetaEntityService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; export const meta = { tags: ['meta'], @@ -14,10 +22,280 @@ export const meta = { res: { type: 'object', - oneOf: [ - { type: 'object', ref: 'MetaLite' }, - { type: 'object', ref: 'MetaDetailed' }, - ], + optional: false, nullable: false, + properties: { + maintainerName: { + type: 'string', + optional: false, nullable: true, + }, + maintainerEmail: { + type: 'string', + optional: false, nullable: true, + }, + version: { + type: 'string', + optional: false, nullable: false, + }, + basedMisskeyVersion: { + type: 'string', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + shortName: { + type: 'string', + optional: false, nullable: true, + }, + uri: { + type: 'string', + optional: false, nullable: false, + format: 'url', + example: 'https://cherrypick.example.com', + }, + description: { + type: 'string', + optional: false, nullable: true, + }, + langs: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + tosUrl: { + type: 'string', + optional: false, nullable: true, + }, + repositoryUrl: { + type: 'string', + optional: false, nullable: false, + default: 'https://github.com/kokonect-link/cherrypick', + }, + feedbackUrl: { + type: 'string', + optional: false, nullable: false, + default: 'https://github.com/kokonect-link/cherrypick/issues/new', + }, + defaultDarkTheme: { + type: 'string', + optional: false, nullable: true, + }, + defaultLightTheme: { + type: 'string', + optional: false, nullable: true, + }, + disableRegistration: { + type: 'boolean', + optional: false, nullable: false, + }, + cacheRemoteFiles: { + type: 'boolean', + optional: false, nullable: false, + }, + cacheRemoteSensitiveFiles: { + type: 'boolean', + optional: false, nullable: false, + }, + emailRequiredForSignup: { + type: 'boolean', + optional: false, nullable: false, + }, + enableHcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + hcaptchaSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + enableRecaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + recaptchaSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + enableTurnstile: { + type: 'boolean', + optional: false, nullable: false, + }, + turnstileSiteKey: { + type: 'string', + optional: false, nullable: true, + }, + swPublickey: { + type: 'string', + optional: false, nullable: true, + }, + mascotImageUrl: { + type: 'string', + optional: false, nullable: false, + default: '/assets/ai.png', + }, + bannerUrl: { + type: 'string', + optional: false, nullable: false, + }, + serverErrorImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + infoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + notFoundImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + iconUrl: { + type: 'string', + optional: false, nullable: true, + }, + maxNoteTextLength: { + type: 'number', + optional: false, nullable: false, + }, + ads: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + url: { + type: 'string', + optional: false, nullable: false, + format: 'url', + }, + place: { + type: 'string', + optional: false, nullable: false, + }, + ratio: { + type: 'number', + optional: false, nullable: false, + }, + imageUrl: { + type: 'string', + optional: false, nullable: false, + format: 'url', + }, + dayOfWeek: { + type: 'integer', + optional: false, nullable: false, + }, + }, + }, + }, + notesPerOneAd: { + type: 'number', + optional: false, nullable: false, + default: 0, + }, + requireSetup: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + enableEmail: { + type: 'boolean', + optional: false, nullable: false, + }, + enableServiceWorker: { + type: 'boolean', + optional: false, nullable: false, + }, + translatorAvailable: { + type: 'boolean', + optional: false, nullable: false, + }, + proxyAccountName: { + type: 'string', + optional: false, nullable: true, + }, + mediaProxy: { + type: 'string', + optional: false, nullable: false, + }, + features: { + type: 'object', + optional: true, nullable: false, + properties: { + registration: { + type: 'boolean', + optional: false, nullable: false, + }, + localTimeline: { + type: 'boolean', + optional: false, nullable: false, + }, + globalTimeline: { + type: 'boolean', + optional: false, nullable: false, + }, + hcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + recaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, + objectStorage: { + type: 'boolean', + optional: false, nullable: false, + }, + serviceWorker: { + type: 'boolean', + optional: false, nullable: false, + }, + miauth: { + type: 'boolean', + optional: true, nullable: false, + default: true, + }, + }, + }, + backgroundImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + impressumUrl: { + type: 'string', + optional: false, nullable: true, + }, + logoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + privacyPolicyUrl: { + type: 'string', + optional: false, nullable: true, + }, + serverRules: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, + themeColor: { + type: 'string', + optional: false, nullable: true, + }, + }, }, } as const; @@ -32,10 +310,117 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - private metaEntityService: MetaEntityService, + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.adsRepository) + private adsRepository: AdsRepository, + + private userEntityService: UserEntityService, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { - return ps.detail ? await this.metaEntityService.packDetailed() : await this.metaEntityService.pack(); + const instance = await this.metaService.fetch(true); + + const ads = await this.adsRepository.createQueryBuilder('ads') + .where('ads.expiresAt > :now', { now: new Date() }) + .andWhere('ads.startsAt <= :now', { now: new Date() }) + .andWhere(new Brackets(qb => { + // 曜日のビットフラグを確認する + qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() }) + .orWhere('ads.dayOfWeek = 0'); + })) + .getMany(); + + const response: any = { + maintainerName: instance.maintainerName, + maintainerEmail: instance.maintainerEmail, + + version: this.config.version, + basedMisskeyVersion: this.config.basedMisskeyVersion, + + name: instance.name, + shortName: instance.shortName, + uri: this.config.url, + description: instance.description, + langs: instance.langs, + tosUrl: instance.termsOfServiceUrl, + repositoryUrl: instance.repositoryUrl, + feedbackUrl: instance.feedbackUrl, + impressumUrl: instance.impressumUrl, + privacyPolicyUrl: instance.privacyPolicyUrl, + disableRegistration: instance.disableRegistration, + emailRequiredForSignup: instance.emailRequiredForSignup, + enableHcaptcha: instance.enableHcaptcha, + hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableRecaptcha: instance.enableRecaptcha, + recaptchaSiteKey: instance.recaptchaSiteKey, + enableTurnstile: instance.enableTurnstile, + turnstileSiteKey: instance.turnstileSiteKey, + swPublickey: instance.swPublicKey, + themeColor: instance.themeColor, + mascotImageUrl: instance.mascotImageUrl, + bannerUrl: instance.bannerUrl, + infoImageUrl: instance.infoImageUrl, + serverErrorImageUrl: instance.serverErrorImageUrl, + notFoundImageUrl: instance.notFoundImageUrl, + iconUrl: instance.iconUrl, + backgroundImageUrl: instance.backgroundImageUrl, + logoImageUrl: instance.logoImageUrl, + maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, + // クライアントの手間を減らすためあらかじめJSONに変換しておく + defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, + defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null, + ads: ads.map(ad => ({ + id: ad.id, + url: ad.url, + place: ad.place, + ratio: ad.ratio, + imageUrl: ad.imageUrl, + dayOfWeek: ad.dayOfWeek, + })), + notesPerOneAd: instance.notesPerOneAd, + enableEmail: instance.enableEmail, + enableServiceWorker: instance.enableServiceWorker, + + // translatorAvailable: instance.deeplAuthKey != null, + translatorAvailable: instance.translatorType != null, + + serverRules: instance.serverRules, + + policies: { ...DEFAULT_POLICIES, ...instance.policies }, + + mediaProxy: this.config.mediaProxy, + + ...(ps.detail ? { + cacheRemoteFiles: instance.cacheRemoteFiles, + cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, + requireSetup: (await this.usersRepository.countBy({ + host: IsNull(), + })) === 0, + } : {}), + }; + + if (ps.detail) { + const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null; + + response.proxyAccountName = proxyAccount ? proxyAccount.username : null; + response.features = { + registration: !instance.disableRegistration, + emailRequiredForSignup: instance.emailRequiredForSignup, + hcaptcha: instance.enableHcaptcha, + recaptcha: instance.enableRecaptcha, + turnstile: instance.enableTurnstile, + objectStorage: instance.useObjectStorage, + serviceWorker: instance.enableServiceWorker, + miauth: true, + }; + } + + return response; }); } } diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index fc9a8f3ebe..1b7d533d01 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index e39c133b43..fd4e19129e 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -83,7 +83,7 @@ export default class extends Endpoint { // eslint- }); // Check if already muting - const exist = await this.mutingsRepository.exists({ + const exist = await this.mutingsRepository.exist({ where: { muterId: muter.id, muteeId: mutee.id, diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index d11832858e..c7c551ac6a 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts index 23204f2829..3755a59cc9 100644 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ b/packages/backend/src/server/api/endpoints/mute/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts index c04a92626f..32a3cec6c5 100644 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ b/packages/backend/src/server/api/endpoints/my/apps.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index d5ae17348c..2da1784e39 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 0c6533d336..f189dedb54 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 29cab9f212..e0dc3890f3 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index d13fd5e82e..758b53a967 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index c220e64b43..7efa791b94 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -34,10 +34,11 @@ describe('api:notes/create', () => { .toBe(VALID); }); - test('null post', () => { - expect(v({ text: null })) - .toBe(INVALID); - }); + // TODO + //test('null post', () => { + // expect(v({ text: null })) + // .toBe(INVALID); + //}); test('0 characters post', () => { expect(v({ text: '' })) @@ -48,11 +49,6 @@ describe('api:notes/create', () => { expect(v({ text: await tooLong })) .toBe(INVALID); }); - - test('whitespace-only post', () => { - expect(v({ text: ' ' })) - .toBe(INVALID); - }); }); describe('cw', () => { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index caa0d84602..d1ab8c1468 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -16,10 +16,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; -import { MetaService } from '@/core/MetaService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { isPureRenote } from '@/misc/is-pure-renote.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -85,12 +82,6 @@ export const meta = { id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', }, - cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: { - message: 'You cannot reply to a specified visibility note with extended visibility.', - code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', - id: 'ed940410-535c-4d5e-bfa3-af798671e93c', - }, - cannotCreateAlreadyExpiredPoll: { message: 'Poll is already expired.', code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', @@ -120,31 +111,13 @@ export const meta = { code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', id: '33510210-8452-094c-6227-4a6c05d99f00', }, - - containsProhibitedWords: { - message: 'Cannot post because it contains prohibited words.', - code: 'CONTAINS_PROHIBITED_WORDS', - id: 'aa6e01d3-a85c-669d-758a-76aab43af334', - }, - - containsTooManyMentions: { - message: 'Cannot post because it exceeds the allowed number of mentions.', - code: 'CONTAINS_TOO_MANY_MENTIONS', - id: '4de0363a-3046-481b-9b0f-feff3e211025', - }, - - cannotScheduleDeleteEarlierThanNow: { - message: 'Cannot specify delete time earlier than now.', - code: 'CANNOT_SCHEDULE_DELETE_EARLIER_THAN_NOW', - id: '9f04994a-3aa2-11ef-a495-177eea74788f', - }, }, } as const; export const paramDef = { type: 'object', properties: { - visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified', 'private'], default: 'public' }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, @@ -208,43 +181,15 @@ export const paramDef = { metadata: { type: 'object' }, }, }, - scheduledDelete: { - type: 'object', - nullable: true, - properties: { - deleteAt: { type: 'integer', nullable: true }, - deleteAfter: { type: 'integer', nullable: true, minimum: 1 }, - }, - }, }, // (re)note with text, files and poll are optional - if: { - properties: { - renoteId: { - type: 'null', - }, - fileIds: { - type: 'null', - }, - mediaIds: { - type: 'null', - }, - poll: { - type: 'null', - }, - }, - }, - then: { - properties: { - text: { - type: 'string', - minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, - pattern: '[^\\s]+', - }, - }, - required: ['text'], - }, + anyOf: [ + { required: ['text'] }, + { required: ['renoteId'] }, + { required: ['fileIds'] }, + { required: ['mediaIds'] }, + { required: ['poll'] }, + ], } as const; @Injectable() @@ -300,13 +245,13 @@ export default class extends Endpoint { // eslint- if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (isRenote(renote) && !isQuote(renote)) { + } else if (isPureRenote(renote)) { throw new ApiError(meta.errors.cannotReRenote); } // Check blocking if (renote.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ + const blockExist = await this.blockingsRepository.exist({ where: { blockerId: renote.userId, blockeeId: me.id, @@ -346,17 +291,15 @@ export default class extends Endpoint { // eslint- if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (isRenote(reply) && !isQuote(reply)) { + } else if (isPureRenote(reply)) { throw new ApiError(meta.errors.cannotReplyToPureRenote); } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) { throw new ApiError(meta.errors.cannotReplyToInvisibleNote); - } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') { - throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); } // Check blocking if (reply.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ + const blockExist = await this.blockingsRepository.exist({ where: { blockerId: reply.userId, blockeeId: me.id, @@ -378,16 +321,6 @@ export default class extends Endpoint { // eslint- } } - if (ps.scheduledDelete) { - if (typeof ps.scheduledDelete.deleteAt === 'number') { - if (ps.scheduledDelete.deleteAt < Date.now()) { - throw new ApiError(meta.errors.cannotScheduleDeleteEarlierThanNow); - } else if (typeof ps.scheduledDelete.deleteAfter === 'number') { - ps.scheduledDelete.deleteAt = Date.now() + ps.scheduledDelete.deleteAfter; - } - } - } - let channel: MiChannel | null = null; if (ps.channelId != null) { channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); @@ -398,51 +331,38 @@ export default class extends Endpoint { // eslint- } // 投稿を作成 - try { - const note = await this.noteCreateService.create(me, { - createdAt: new Date(), - files: files, - poll: ps.poll ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple ?? false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - } : undefined, - text: ps.text ?? undefined, - reply, - renote, - event: ps.event ? { - start: new Date(ps.event.start!), - end: ps.event.end ? new Date(ps.event.end) : null, - title: ps.event.title!, - metadata: ps.event.metadata ?? {}, - } : undefined, - cw: ps.cw, - localOnly: ps.localOnly, - reactionAcceptance: ps.reactionAcceptance, - disableRightClick: ps.disableRightClick, - visibility: ps.visibility, - visibleUsers, - channel, - apMentions: ps.noExtractMentions ? [] : undefined, - apHashtags: ps.noExtractHashtags ? [] : undefined, - apEmojis: ps.noExtractEmojis ? [] : undefined, - deleteAt: ps.scheduledDelete?.deleteAt ? new Date(ps.scheduledDelete.deleteAt) : ps.scheduledDelete?.deleteAfter ? new Date(Date.now() + ps.scheduledDelete.deleteAfter) : null, - }); - - return { - createdNote: await this.noteEntityService.pack(note, me), - }; - } catch (e) { - // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい - if (e instanceof IdentifiableError) { - if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { - throw new ApiError(meta.errors.containsProhibitedWords); - } else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') { - throw new ApiError(meta.errors.containsTooManyMentions); - } - } - throw e; - } + const note = await this.noteCreateService.create(me, { + createdAt: new Date(), + files: files, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : undefined, + text: ps.text ?? undefined, + reply, + renote, + event: ps.event ? { + start: new Date(ps.event.start!), + end: ps.event.end ? new Date(ps.event.end) : null, + title: ps.event.title!, + metadata: ps.event.metadata ?? {}, + } : undefined, + cw: ps.cw, + localOnly: ps.localOnly, + reactionAcceptance: ps.reactionAcceptance, + disableRightClick: ps.disableRightClick, + visibility: ps.visibility, + visibleUsers, + channel, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, + }); + + return { + createdNote: await this.noteEntityService.pack(note, me), + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 9d7c9a9081..37038bb3ce 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/events/search.ts b/packages/backend/src/server/api/endpoints/notes/events/search.ts index b917c8fa36..de6352e7a4 100644 --- a/packages/backend/src/server/api/endpoints/notes/events/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/events/search.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index 804071b3d4..5918614a1d 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -67,7 +67,7 @@ export default class extends Endpoint { // eslint- }); // if already favorited - const exist = await this.noteFavoritesRepository.exists({ + const exist = await this.noteFavoritesRepository.exist({ where: { noteId: note.id, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 2036facdba..403d6b0d53 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index dcd971360d..abbcb63008 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 91feb48e03..dece70915c 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -42,7 +42,6 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withCats: { type: 'boolean', default: false }, - withoutBots: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -103,10 +102,6 @@ export default class extends Endpoint { // eslint- if (ps.withCats) { query.andWhere('(select "isCat" from "user" where id = note."userId")'); } - - if (ps.withoutBots) { - query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); - } //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index bf515299a6..3d6a551e36 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -68,7 +68,6 @@ export const paramDef = { withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, withCats: { type: 'boolean', default: false }, - withoutBots: { type: 'boolean', default: false }, }, required: [], } as const; @@ -116,7 +115,6 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, withCats: ps.withCats, - withoutBots: ps.withoutBots, }, me); process.nextTick(() => { @@ -143,16 +141,9 @@ export default class extends Endpoint { // eslint- timelineConfig = [ `homeTimeline:${me.id}`, 'localTimeline', - `localTimelineWithReplyTo:${me.id}`, ]; } - const [ - followings, - ] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(me.id), - ]); - const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -164,14 +155,6 @@ export default class extends Endpoint { // eslint- alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, withCats: ps.withCats, - withoutBots: ps.withoutBots, - noteFilter: note => { - if (note.reply && note.reply.visibility === 'followers') { - if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; - } - - return true; - }, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, @@ -182,7 +165,6 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, withCats: ps.withCats, - withoutBots: ps.withoutBots, }, me), }); @@ -204,7 +186,6 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withReplies: boolean, withCats: boolean, - withoutBots: boolean, }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); const followingChannels = await this.channelFollowingsRepository.find({ @@ -228,8 +209,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('(SELECT "isSensitive" FROM "user" WHERE id = note."userId") = FALSE'); + .leftJoinAndSelect('renote.user', 'renoteUser'); if (followingChannels.length > 0) { const followingChannelIds = followingChannels.map(x => x.followeeId); @@ -296,10 +276,6 @@ export default class extends Endpoint { // eslint- if (ps.withCats) { query.andWhere('(select "isCat" from "user" where id = note."userId")'); } - - if (ps.withoutBots) { - query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); - } //#endregion return await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 792f294959..c162295ec9 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -54,7 +54,6 @@ export const paramDef = { withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, withCats: { type: 'boolean', default: false }, - withoutBots: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -101,7 +100,6 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, withCats: ps.withCats, - withoutBots: ps.withoutBots, }, me); process.nextTick(() => { @@ -128,7 +126,6 @@ export default class extends Endpoint { // eslint- alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, withCats: ps.withCats, - withoutBots: ps.withoutBots, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, @@ -136,7 +133,6 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, withCats: ps.withCats, - withoutBots: ps.withoutBots, }, me), }); @@ -157,7 +153,6 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withReplies: boolean, withCats: boolean, - withoutBots: boolean, }, me: MiLocalUser | null) { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -166,8 +161,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('(SELECT "isSensitive" FROM "user" WHERE id = note."userId") = FALSE'); + .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); @@ -194,10 +188,6 @@ export default class extends Endpoint { // eslint- query.andWhere('(select "isCat" from "user" where id = note."userId")'); } - if (ps.withoutBots) { - query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); - } - return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 5558dd3a8b..1c6edab1e7 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -61,9 +61,9 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere(new Brackets(qb => { - qb // このmeIdAsListパラメータはqueryServiceのgenerateVisibilityQueryでセットされる - .where(':meIdAsList <@ note.mentions') - .orWhere(':meIdAsList <@ note.visibleUserIds'); + qb + .where(`'{"${me.id}"}' <@ note.mentions`) + .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); })) // Avoid scanning primary key index .orderBy('CONCAT(note.id)', 'DESC') diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 4fd6f8682d..d112844324 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -32,7 +32,6 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, - excludeChannels: { type: 'boolean', default: false }, }, required: [], } as const; @@ -87,12 +86,6 @@ export default class extends Endpoint { // eslint- query.setParameters(mutingQuery.getParameters()); //#endregion - //#region exclude channels - if (ps.excludeChannels) { - query.andWhere('poll.channelId IS NULL'); - } - //#endregion - const polls = await query .orderBy('poll.noteId', 'DESC') .limit(ps.limit) diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index f33f49075b..1509705b16 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -144,12 +144,12 @@ export default class extends Endpoint { // eslint- } // Create vote - const vote = await this.pollVotesRepository.insertOne({ + const vote = await this.pollVotesRepository.insert({ id: this.idService.gen(createdAt.getTime()), noteId: note.id, userId: me.id, choice: ps.choice, - }); + }).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0])); // Increment votes count const index = ps.choice + 1; // In SQL, array index is 1 based diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 97b12ab7f7..3d1af099f1 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -76,7 +76,7 @@ export default class extends Endpoint { // eslint- const reactions = await query.limit(ps.limit).getMany(); - return await this.noteReactionEntityService.packMany(reactions, me); + return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me))); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 0f0dcca605..f118064a8d 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -36,12 +36,6 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: '20ef5475-9f38-4e4c-bd33-de6d979498ec', }, - - cannotReactToRenote: { - message: 'You cannot react to Renote.', - code: 'CANNOT_REACT_TO_RENOTE', - id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', - }, }, } as const; @@ -68,7 +62,6 @@ export default class extends Endpoint { // eslint- await this.reactionService.create(me, note, ps.reaction).catch(err => { if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); - if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote); throw err; }); return; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index e6c3bbbcf5..c5bc142a2b 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index ffe1ee6eb8..6fb569649a 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 5f32332a6a..d6492f68a5 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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 626ff080c7..26826bef7c 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 @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -87,14 +87,14 @@ export default class extends Endpoint { // eslint- try { if (ps.tag) { if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); - query.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(ps.tag)] }); + query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); } else { query.andWhere(new Brackets(qb => { for (const tags of ps.query!) { qb.orWhere(new Brackets(qb => { for (const tag of tags) { if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection'); - qb.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(tag)] }); + qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); } })); } diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index e1d40d9233..b6cf556d1f 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -41,6 +41,7 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + origin: { type: 'string', enum: ['local', 'remote', 'combined'], default: 'combined' }, offset: { type: 'integer', default: 0 }, host: { type: 'string', @@ -48,9 +49,6 @@ export const paramDef = { }, userId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, - fileOption: { type: 'string', enum: ['combined', 'fileOnly', 'noFile'], default: 'combined' }, - excludeNsfw: { type: 'boolean', default: false }, - excludeBot: { type: 'boolean', default: false }, }, required: ['query'], } as const; @@ -74,9 +72,7 @@ export default class extends Endpoint { // eslint- userId: ps.userId, channelId: ps.channelId, host: ps.host, - fileOption: ps.fileOption, - excludeNsfw: ps.excludeNsfw, - excludeBot: ps.excludeBot, + origin: ps.origin, }, { untilId: ps.untilId, sinceId: ps.sinceId, diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index adcda30a7d..a8bc93aead 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 4c1eb86542..e0725d3b42 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index 732d644a29..374ef6438d 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index d94d6cd652..80ac0bae26 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index f3f69d2046..26cd358a1f 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -50,7 +50,6 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withCats: { type: 'boolean', default: false }, - withoutBots: { type: 'boolean', default: false }, }, required: [], } as const; @@ -90,7 +89,6 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withCats: ps.withCats, - withoutBots: ps.withoutBots, }, me); process.nextTick(() => { @@ -117,10 +115,9 @@ export default class extends Endpoint { // eslint- alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, withCats: ps.withCats, - withoutBots: ps.withoutBots, noteFilter: note => { if (note.reply && note.reply.visibility === 'followers') { - if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; + if (!Object.hasOwn(followings, note.reply.userId)) return false; } return true; @@ -135,7 +132,6 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withCats: ps.withCats, - withoutBots: ps.withoutBots, }, me), }); @@ -147,18 +143,7 @@ export default class extends Endpoint { // eslint- }); } - private async getFromDb(ps: { - untilId: string | null; - sinceId: string | null; - limit: number; - includeMyRenotes: boolean; - includeRenotedMyNotes: boolean; - includeLocalRenotes: boolean; - withFiles: boolean; - withRenotes: boolean; - withCats: boolean; - withoutBots: boolean; - }, me: MiLocalUser) { + private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; withCats: boolean; }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); const followingChannels = await this.channelFollowingsRepository.find({ where: { @@ -172,8 +157,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('user.isSensitive = FALSE'); + .leftJoinAndSelect('renote.user', 'renoteUser'); if (followees.length > 0 && followingChannels.length > 0) { // ユーザー・チャンネルともにフォローあり @@ -265,10 +249,6 @@ export default class extends Endpoint { // eslint- if (ps.withCats) { query.andWhere('(select "isCat" from "user" where id = note."userId")'); } - - if (ps.withoutBots) { - query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); - } //#endregion return await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index a1a135601e..42ec0b45f2 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -25,7 +25,7 @@ export const meta = { res: { type: 'object', - optional: true, nullable: false, + optional: false, nullable: false, properties: { sourceLang: { type: 'string' }, text: { type: 'string' }, @@ -43,11 +43,6 @@ export const meta = { code: 'NO_SUCH_NOTE', id: 'bea9b03f-36e0-49c5-a4db-627a029f8971', }, - cannotTranslateInvisibleNote: { - message: 'Cannot translate invisible note.', - code: 'CANNOT_TRANSLATE_INVISIBLE_NOTE', - id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d', - }, noTranslateService: { message: 'Translate service is not available.', code: 'NO_TRANSLATE_SERVICE', @@ -86,11 +81,11 @@ export default class extends Endpoint { // eslint- }); if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) { - throw new ApiError(meta.errors.cannotTranslateInvisibleNote); + return 204; // TODO: 良い感じのエラー返す } if (note.text == null) { - return; + return 204; } const instance = await this.metaService.fetch(); @@ -111,7 +106,7 @@ export default class extends Endpoint { // eslint- let translationResult; if (instance.translatorType === 'deepl') { if (instance.deeplAuthKey == null) { - throw new ApiError(meta.errors.unavailable); + return 204; // TODO: 良い感じのエラー返す } translationResult = await this.translateDeepL((note.cw ? note.cw + '\n' : '') + note.text, targetLang, instance.deeplAuthKey, instance.deeplIsPro, instance.translatorType); } else if (instance.translatorType === 'google_no_api') { @@ -126,9 +121,9 @@ export default class extends Endpoint { // eslint- translator: translatorServices, }; } else if (instance.translatorType === 'ctav3') { - if (instance.ctav3SaKey == null) return Promise.resolve(204); - else if (instance.ctav3ProjectId == null) return Promise.resolve(204); - else if (instance.ctav3Location == null) return Promise.resolve(204); + if (instance.ctav3SaKey == null) return 204; + else if (instance.ctav3ProjectId == null) return 204; + else if (instance.ctav3Location == null) return 204; translationResult = await this.apiCloudTranslationAdvanced( (note.cw ? note.cw + '\n' : '') + note.text, targetLang, instance.ctav3SaKey, instance.ctav3ProjectId, instance.ctav3Location, instance.ctav3Model, instance.ctav3Glossary, instance.translatorType, ); @@ -136,11 +131,11 @@ export default class extends Endpoint { // eslint- throw new Error('Unsupported translator type'); } - return Promise.resolve({ - sourceLang: translationResult.sourceLang || '', - text: translationResult.text || '', - translator: translationResult.translator || [], - }); + return { + sourceLang: translationResult.sourceLang, + text: translationResult.text, + translator: translationResult.translator, + }; }); } @@ -162,11 +157,11 @@ export default class extends Endpoint { // eslint- }); const json = (await res.json()) as { - translations: { - detected_source_language: string; - text: string; - }[]; - }; + translations: { + detected_source_language: string; + text: string; + }[]; + }; return { sourceLang: json.translations[0].detected_source_language, diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 73e70cfde4..b09aa3281a 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts index fecd0baf21..55d11c98fb 100644 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -95,7 +95,7 @@ export const paramDef = { } as const; @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -104,10 +104,7 @@ export default class extends Endpoint { private noteEntityService: NoteEntityService, private noteUpdateService: NoteUpdateService, ) { - super({ - ...meta, - requireRolePolicy: 'canEditNote', // 修正された部分 - }, paramDef, async (ps, me) => { + super(meta, paramDef, async (ps, me) => { const note = await this.getterService.getNote(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 42a89b004a..d6f7537aa9 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 7671b58e6b..789aa53829 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notifications/delete.ts b/packages/backend/src/server/api/endpoints/notifications/delete.ts deleted file mode 100644 index fef3236ae7..0000000000 --- a/packages/backend/src/server/api/endpoints/notifications/delete.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { NotificationService } from '@/core/NotificationService.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['notification', 'account'], - - requireCredential: true, - - kind: 'write:notifications', - - errors: { - 'noSuchNotification': { - message: 'No such notification', - code: 'NO_SUCH_NOTIFICATION', - id: '4818a20e-3d02-11ef-9c7c-63e2e6b43b02', - }, - }, -} as const; - - -export const paramDef = { - type: 'object', - properties: { - notificationId: { type: 'string', format: 'misskey:id' }, - }, - required: ['notificationId'], -} as const; - -@Injectable() -export default class extends Endpoint { - constructor( - private notificationService: NotificationService, - ) { - super(meta, paramDef, async (ps, me) => { - const res = await this.notificationService.deleteNotification(me.id, ps.notificationId); - if (!res) { - throw new ApiError(meta.errors.noSuchNotification); - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/notifications/flush.ts b/packages/backend/src/server/api/endpoints/notifications/flush.ts deleted file mode 100644 index 47c0642fd1..0000000000 --- a/packages/backend/src/server/api/endpoints/notifications/flush.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { NotificationService } from '@/core/NotificationService.js'; - -export const meta = { - tags: ['notifications', 'account'], - - requireCredential: true, - - kind: 'write:notifications', -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - private notificationService: NotificationService, - ) { - super(meta, paramDef, async (ps, me) => { - this.notificationService.flushAllNotifications(me.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 6565125c00..02ca01cf18 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/notifications/test-notification.ts b/packages/backend/src/server/api/endpoints/notifications/test-notification.ts index 50b850a519..ac0fc8bd3f 100644 --- a/packages/backend/src/server/api/endpoints/notifications/test-notification.ts +++ b/packages/backend/src/server/api/endpoints/notifications/test-notification.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index ce454ab24a..8493ccb65b 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -55,7 +55,7 @@ export default class extends Endpoint { // eslint- var: ps.var, userId: me.id, user: await this.userEntityService.pack(me.id, { id: page.userId }, { - schema: 'UserDetailed', + detail: true, }), }); }); diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index fa03b0b457..8538019484 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -102,7 +102,7 @@ export default class extends Endpoint { // eslint- } }); - const page = await this.pagesRepository.insertOne(new MiPage({ + const page = await this.pagesRepository.insert(new MiPage({ id: this.idService.gen(), updatedAt: new Date(), title: ps.title, @@ -117,7 +117,7 @@ export default class extends Endpoint { // eslint- alignCenter: ps.alignCenter, hideTitleWhenPinned: ps.hideTitleWhenPinned, font: ps.font, - })); + })).then(x => this.pagesRepository.findOneByOrFail(x.identifiers[0])); return await this.pageEntityService.pack(page); }); diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index aa2ba75a41..c2b2630355 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index a47b69e56e..fc71209ee0 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 11eed693ad..433a19439c 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -70,7 +70,7 @@ export default class extends Endpoint { // eslint- } // if already liked - const exist = await this.pageLikesRepository.exists({ + const exist = await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index e08b832a3f..03fe752e31 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 70c965e0ad..55d3eea6d7 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index f11bbbcb1a..75a5a263c7 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -70,7 +70,7 @@ export const paramDef = { alignCenter: { type: 'boolean' }, hideTitleWhenPinned: { type: 'boolean' }, }, - required: ['pageId'], + required: ['pageId', 'title', 'name', 'content', 'variables', 'script'], } as const; @Injectable() @@ -91,8 +91,9 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } + let eyeCatchingImage = null; if (ps.eyeCatchingImageId != null) { - const eyeCatchingImage = await this.driveFilesRepository.findOneBy({ + eyeCatchingImage = await this.driveFilesRepository.findOneBy({ id: ps.eyeCatchingImageId, userId: me.id, }); @@ -115,15 +116,23 @@ export default class extends Endpoint { // eslint- await this.pagesRepository.update(page.id, { updatedAt: new Date(), title: ps.title, - name: ps.name, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + name: ps.name === undefined ? page.name : ps.name, summary: ps.summary === undefined ? page.summary : ps.summary, content: ps.content, variables: ps.variables, script: ps.script, - alignCenter: ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned, - font: ps.font, - eyeCatchingImageId: ps.eyeCatchingImageId, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + font: ps.font === undefined ? page.font : ps.font, + eyeCatchingImageId: ps.eyeCatchingImageId === null + ? null + : ps.eyeCatchingImageId === undefined + ? page.eyeCatchingImageId + : eyeCatchingImage!.id, }); }); } diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts index e218a8f755..262d015830 100644 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ b/packages/backend/src/server/api/endpoints/ping.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 15832ef7f8..bb47575c35 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -52,7 +52,7 @@ export default class extends Endpoint { // eslint- host: acct.host ?? IsNull(), }))); - return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 9f7d078014..e07fc66b63 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -49,7 +49,7 @@ export default class extends Endpoint { // eslint- throw err; }); - const exist = await this.promoReadsRepository.exists({ + const exist = await this.promoReadsRepository.exist({ where: { noteId: note.id, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index 84a1f010d4..cf7a9126d0 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -1,16 +1,17 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import type { RenoteMutingsRepository } from '@/models/_.js'; +import type { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; -import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js"; -import type { RenoteMutingsRepository } from '@/models/_.js'; export const meta = { tags: ['account'], @@ -61,7 +62,7 @@ export default class extends Endpoint { // eslint- private renoteMutingsRepository: RenoteMutingsRepository, private getterService: GetterService, - private userRenoteMutingService: UserRenoteMutingService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const muter = me; @@ -72,25 +73,27 @@ export default class extends Endpoint { // eslint- } // Get mutee - const mutee = await this.getterService.getUser(ps.userId).catch(err => { + const mutee = await getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; }); // Check if already muting - const exist = await this.renoteMutingsRepository.exists({ - where: { - muterId: muter.id, - muteeId: mutee.id, - }, + const exist = await this.renoteMutingsRepository.findOneBy({ + muterId: muter.id, + muteeId: mutee.id, }); - if (exist === true) { + if (exist != null) { throw new ApiError(meta.errors.alreadyMuting); } // Create mute - await this.userRenoteMutingService.mute(muter, mutee); + await this.renoteMutingsRepository.insert({ + id: this.idService.gen(), + muterId: muter.id, + muteeId: mutee.id, + } as MiRenoteMuting); }); } } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index 1a584b8404..b05c6a8ace 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -1,15 +1,14 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RenoteMutingsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; -import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js"; -import type { RenoteMutingsRepository } from '@/models/_.js'; export const meta = { tags: ['account'], @@ -54,7 +53,6 @@ export default class extends Endpoint { // eslint- private renoteMutingsRepository: RenoteMutingsRepository, private getterService: GetterService, - private userRenoteMutingService: UserRenoteMutingService, ) { super(meta, paramDef, async (ps, me) => { const muter = me; @@ -81,7 +79,9 @@ export default class extends Endpoint { // eslint- } // Delete mute - await this.userRenoteMutingService.unmute([exist]); + await this.renoteMutingsRepository.delete({ + id: exist.id, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts index 3be01f989a..5614b95c03 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/list.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 86fe6a2e6e..a0560745ec 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 67d5fabd86..195b0dfe4c 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 4f2cfbc958..7a10c758be 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { hashPassword } from '@/misc/password.js'; +import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -53,7 +53,8 @@ export default class extends Endpoint { // eslint- } // Generate hash of password - const hash = await hashPassword(ps.password); + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(ps.password, salt); await this.userProfilesRepository.update(req.userId, { password: hash, diff --git a/packages/backend/src/server/api/endpoints/retention.ts b/packages/backend/src/server/api/endpoints/retention.ts index 4695f32042..372f0e478b 100644 --- a/packages/backend/src/server/api/endpoints/retention.ts +++ b/packages/backend/src/server/api/endpoints/retention.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,32 +14,6 @@ export const meta = { requireCredential: false, res: { - type: 'array', - items: { - type: 'object', - properties: { - createdAt: { - type: 'string', - format: 'date-time', - }, - users: { - type: 'number', - }, - data: { - type: 'object', - additionalProperties: { - anyOf: [{ - type: 'number', - }], - }, - }, - }, - required: [ - 'createdAt', - 'users', - 'data', - ], - }, }, allowGet: true, diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index b087aa242b..2f6da65250 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 71f2782a5d..d35adbbc05 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts index 38477c5e8e..8ca223f977 100644 --- a/packages/backend/src/server/api/endpoints/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 48d350af59..1d18bd6757 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -37,7 +37,7 @@ export const meta = { }, user: { type: 'object', - ref: 'UserDetailed', + ref: 'User', }, }, required: ['id', 'user'], @@ -92,12 +92,9 @@ export default class extends Endpoint { // eslint- .limit(ps.limit) .getMany(); - const _users = assigns.map(({ user, userId }) => user ?? userId); - const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) - .then(users => new Map(users.map(u => [u.id, u]))); return await Promise.all(assigns.map(async assign => ({ id: assign.id, - user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), + user: await this.userEntityService.pack(assign.user!, me, { detail: true }), }))); }); } diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index c13802eb06..86948b8795 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index 1e6983177f..965380aeda 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index a9a33149f9..6b74ba4b60 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -9,7 +9,6 @@ import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; export const meta = { tags: ['account'], @@ -67,7 +66,6 @@ export default class extends Endpoint { // eslint- private idService: IdService, private metaService: MetaService, - private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { // if already subscribed @@ -99,8 +97,6 @@ export default class extends Endpoint { // eslint- sendReadMessage: ps.sendReadMessage, }); - this.pushNotificationService.refreshCache(me.id); - return { state: 'subscribed' as const, key: instance.swPublicKey, diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts index 797e4fd34d..4f1f64727c 100644 --- a/packages/backend/src/server/api/endpoints/sw/show-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index 2edf7fab1b..b3d69c4736 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; export const meta = { tags: ['account'], @@ -30,18 +29,12 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, - - private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { await this.swSubscriptionsRepository.delete({ ...(me ? { userId: me.id } : {}), endpoint: ps.endpoint, }); - - if (me) { - this.pushNotificationService.refreshCache(me.id); - } }); } } diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts index 839a07c770..ad311d6985 100644 --- a/packages/backend/src/server/api/endpoints/sw/update-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -59,8 +58,6 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, - - private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { const swSubscription = await this.swSubscriptionsRepository.findOneBy({ @@ -80,8 +77,6 @@ export default class extends Endpoint { // eslint- sendReadMessage: swSubscription.sendReadMessage, }); - this.pushNotificationService.refreshCache(me.id); - return { userId: swSubscription.userId, endpoint: swSubscription.endpoint, diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 9231f0ab94..1ec8d00481 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -19,24 +19,20 @@ export const meta = { id: { type: 'string', format: 'misskey:id', - optional: true, nullable: false, }, required: { type: 'boolean', - optional: false, nullable: false, }, string: { type: 'string', - optional: true, nullable: false, }, default: { type: 'string', - optional: true, nullable: false, }, nullableDefault: { type: 'string', default: 'hello', - optional: true, nullable: true, + nullable: true, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index affb0996f1..1b26a0dd14 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index e845853017..ff710c6350 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -89,7 +89,7 @@ export default class extends Endpoint { // eslint- const users = await query.getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts index f7139b3684..88a85c6d15 100644 --- a/packages/backend/src/server/api/endpoints/users/achievements.ts +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 7f7d2ea8cc..dba9783462 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts index e01f19ba7a..42e3ed9a37 100644 --- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/flashs.ts b/packages/backend/src/server/api/endpoints/users/flashs.ts index e5ea450215..7e8e9ddc7a 100644 --- a/packages/backend/src/server/api/endpoints/users/flashs.ts +++ b/packages/backend/src/server/api/endpoints/users/flashs.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 7ce7734f53..bfa1c7d751 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -101,7 +101,7 @@ export default class extends Endpoint { // eslint- if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ + const isFollowing = await this.followingsRepository.exist({ where: { followeeId: user.id, followerId: me.id, diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 6b3389f0b2..4b18df7228 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,12 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; -import { birthdaySchema } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; @@ -67,7 +66,7 @@ export const paramDef = { description: 'The local host is represented with `null`.', }, - birthday: { ...birthdaySchema, nullable: true }, + birthday: { type: 'string', nullable: true }, }, anyOf: [ { required: ['userId'] }, @@ -110,7 +109,7 @@ export default class extends Endpoint { // eslint- if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ + const isFollowing = await this.followingsRepository.exist({ where: { followeeId: user.id, followerId: me.id, @@ -128,7 +127,9 @@ export default class extends Endpoint { // eslint- if (ps.birthday) { try { - const birthday = ps.birthday.substring(5, 10); + const d = new Date(ps.birthday); + d.setHours(0, 0, 0, 0); + const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); birthdayUserQuery.select('user_profile.userId') .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`); diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index 553886374c..465f322627 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 9248a2fa68..e446475a2f 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -118,14 +118,12 @@ export default class extends Endpoint { // eslint- const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); // Extract top replied users - const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit); + const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); // Make replies object (includes weights) - const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' }) - .then(users => new Map(users.map(u => [u.id, u]))); - const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({ - user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }), - weight: repliedUsers[userId] / peak, + const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ + user: await this.userEntityService.pack(user, me, { detail: true }), + weight: repliedUsers[user] / peak, }))); return repliesObj; diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts index ec9cfc064f..0bcc4bf7be 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -55,11 +55,11 @@ export default class extends Endpoint { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const userGroup = await this.userGroupsRepository.insertOne({ + const userGroup = await this.userGroupsRepository.insert({ id: this.idService.gen(), userId: me.id, name: ps.name, - } as MiUserGroup); + } as MiUserGroup).then(x => this.userGroupsRepository.findOneByOrFail(x.identifiers[0])); // Push the owner await this.userGroupJoiningsRepository.insert({ diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts index 9a8112ffef..3ed21b8e3b 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts index 968691d552..e790d000bc 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts index fa7d40e28c..0740845ecf 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index b9998cfa19..aa5488e271 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -109,11 +109,11 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.alreadyInvited); } - const invitation = await this.userGroupInvitationsRepository.insertOne({ + const invitation = await this.userGroupInvitationsRepository.insert({ id: this.idService.gen(), userId: user.id, userGroupId: userGroup.id, - } as MiUserGroupInvitation); + } as MiUserGroupInvitation).then(x => this.userGroupInvitationsRepository.findOneByOrFail(x.identifiers[0])); // 通知を作成 this.notificationService.createNotification(user.id, 'groupInvited', { diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts index e34e8d3d38..42bd0e4b6e 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts index ed105fc4b6..d2f9d79caf 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts index 1c456e8686..7ae5893840 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts index 41838540e4..df429d7f3b 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts index 898b967d01..e6dcd53bde 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts index cb9ff1ebcd..83f3a93d9b 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts index ce887c1647..08e6bf1c38 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index 7e44d501ab..3b40a28e96 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -90,7 +90,7 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const listExist = await this.userListsRepository.exists({ + const listExist = await this.userListsRepository.exist({ where: { id: ps.listId, isPublic: true, @@ -100,15 +100,15 @@ export default class extends Endpoint { // eslint- const currentCount = await this.userListsRepository.countBy({ userId: me.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) { + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { throw new ApiError(meta.errors.tooManyUserLists); } - const userList = await this.userListsRepository.insertOne({ + const userList = await this.userListsRepository.insert({ id: this.idService.gen(), userId: me.id, name: ps.name, - } as MiUserList); + } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); const users = (await this.userListMembershipsRepository.findBy({ userListId: ps.listId, @@ -121,7 +121,7 @@ export default class extends Endpoint { // eslint- }); if (currentUser.id !== me.id) { - const blockExist = await this.blockingsRepository.exists({ + const blockExist = await this.blockingsRepository.exist({ where: { blockerId: currentUser.id, blockeeId: me.id, @@ -132,7 +132,7 @@ export default class extends Endpoint { // eslint- } } - const exist = await this.userListMembershipsRepository.exists({ + const exist = await this.userListMembershipsRepository.exist({ where: { userListId: userList.id, userId: currentUser.id, diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index 7daf05ba4e..cb035e76f7 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -61,15 +61,15 @@ export default class extends Endpoint { // eslint- const currentCount = await this.userListsRepository.countBy({ userId: me.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) { + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { throw new ApiError(meta.errors.tooManyUserLists); } - const userList = await this.userListsRepository.insertOne({ + const userList = await this.userListsRepository.insert({ id: this.idService.gen(), userId: me.id, name: ps.name, - } as MiUserList); + } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); return await this.userListEntityService.pack(userList); }); diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index dc0d28a0eb..ce2f2b813a 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts index fd142d5a01..8d1b8a6157 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -47,7 +47,7 @@ export default class extends Endpoint { private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const userListExist = await this.userListsRepository.exists({ + const userListExist = await this.userListsRepository.exist({ where: { id: ps.listId, isPublic: true, @@ -58,7 +58,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchList); } - const exist = await this.userListFavoritesRepository.exists({ + const exist = await this.userListFavoritesRepository.exist({ where: { userId: me.id, userListId: ps.listId, diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts index 6d6e8d34ea..dfec7ed6e7 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -46,7 +46,7 @@ export const meta = { }, user: { type: 'object', - ref: 'UserLite', + ref: 'User', }, withReplies: { type: 'boolean', diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 4241ef1cd0..631d254fda 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 94f06f3bea..36617f33eb 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index c717b3959c..2cfe061b89 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -104,7 +104,7 @@ export default class extends Endpoint { // eslint- // Check blocking if (user.id !== me.id) { - const blockExist = await this.blockingsRepository.exists({ + const blockExist = await this.blockingsRepository.exist({ where: { blockerId: user.id, blockeeId: me.id, @@ -115,7 +115,7 @@ export default class extends Endpoint { // eslint- } } - const exist = await this.userListMembershipsRepository.exists({ + const exist = await this.userListMembershipsRepository.exist({ where: { userListId: userList.id, userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 8756801fe4..5993708348 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -74,7 +74,7 @@ export default class extends Endpoint { userListId: ps.listId, }); if (me !== null) { - additionalProperties.isLiked = await this.userListFavoritesRepository.exists({ + additionalProperties.isLiked = await this.userListFavoritesRepository.exist({ where: { userId: me.id, userListId: ps.listId, diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts index 3f4bd5af8c..a9841c00b8 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -45,7 +45,7 @@ export default class extends Endpoint { private userListFavoritesRepository: UserListFavoritesRepository, ) { super(meta, paramDef, async (ps, me) => { - const userListExist = await this.userListsRepository.exists({ + const userListExist = await this.userListsRepository.exist({ where: { id: ps.listId, isPublic: true, diff --git a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts index 3948ae1685..a9cc82d36d 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index a38f84d7b0..8670987371 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index e1f2a13802..2bdfeb9677 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -53,7 +53,6 @@ export const paramDef = { withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withChannelNotes: { type: 'boolean', default: false }, - withoutBots: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -106,7 +105,6 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withCats: ps.withCats, - withoutBots: ps.withoutBots, }, me); return await this.noteEntityService.packMany(timeline, me); @@ -132,7 +130,6 @@ export default class extends Endpoint { // eslint- excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files excludePureRenotes: !ps.withRenotes, withCats: ps.withCats, - withoutBots: ps.withoutBots, noteFilter: note => { if (note.channel?.isSensitive && !isSelf) return false; if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; @@ -149,7 +146,6 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withCats: ps.withCats, - withoutBots: ps.withoutBots, }, me), }); @@ -166,7 +162,6 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withCats: boolean, withRenotes: boolean, - withoutBots: boolean, }, me: MiLocalUser | null) { const isSelf = me && (me.id === ps.userId); @@ -212,10 +207,6 @@ export default class extends Endpoint { // eslint- query.andWhere('(select "isCat" from "user" where id = note."userId")'); } - if (ps.withoutBots) { - query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); - } - return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index bb7de0e0b5..625d7a71c4 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 7805ae3288..f4cb15363d 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -1,18 +1,15 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, NoteReactionsRepository } from '@/models/_.js'; +import type { UserProfilesRepository, NotesRepository, NoteReactionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; import { DI } from '@/di-symbols.js'; -import { CacheService } from '@/core/CacheService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; +import { MiNoteReaction } from '@/models/_.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -38,11 +35,6 @@ export const meta = { code: 'REACTIONS_NOT_PUBLIC', id: '673a7dd2-6924-1093-e0c0-e68456ceae5c', }, - isRemoteUser: { - message: 'Currently unavailable to display reactions of remote users. See https://github.com/misskey-dev/misskey/issues/12964', - code: 'IS_REMOTE_USER', - id: '6b95fa98-8cf9-2350-e284-f0ffdb54a805', - }, }, } as const; @@ -65,55 +57,41 @@ export default class extends Endpoint { // eslint- @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + @Inject(DI.noteReactionsRepository) private noteReactionsRepository: NoteReactionsRepository, - private cacheService: CacheService, - private userEntityService: UserEntityService, private noteReactionEntityService: NoteReactionEntityService, private queryService: QueryService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set(); - const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users - if (!iAmModerator) { - const user = await this.cacheService.findUserById(ps.userId); - if (this.userEntityService.isRemoteUser(user)) { - throw new ApiError(meta.errors.isRemoteUser); - } - - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); - if ((me == null || me.id !== ps.userId) && !profile.publicReactions) { - throw new ApiError(meta.errors.reactionsNotPublic); - } - - // early return if me is blocked by requesting user - if (userIdsWhoBlockingMe.has(ps.userId)) { - return []; - } - } + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); - const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set(); + if ((me == null || me.id !== ps.userId) && !profile.publicReactions) { + throw new ApiError(meta.errors.reactionsNotPublic); + } - const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('reaction.userId = :userId', { userId: ps.userId }) - .leftJoinAndSelect('reaction.note', 'note'); + const query = this.notesRepository.createQueryBuilder('note') + .innerJoinAndSelect(qb => + this.queryService.makePaginationQuery( + qb + .from(this.noteReactionsRepository.metadata.targetName, 'reaction') + .where('"reaction"."userId" = :userId', { userId: ps.userId }), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate, + ), + 'reaction', + '"reaction"."noteId" = note.id', + ); this.queryService.generateVisibilityQuery(query, me); - const reactions = (await query + const reactions = await query .limit(ps.limit) - .getMany()).filter(reaction => { - if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user - if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false; - - return true; - }); + .getRawMany(); - return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true }); + return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me, { withNote: true }))); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 5b3b4527f7..f05fd14200 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -76,7 +76,7 @@ export default class extends Endpoint { // eslint- const users = await query.limit(ps.limit).offset(ps.offset).getMany(); - return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users, me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 1d75437b81..eaa190d34c 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -132,9 +132,11 @@ export default class extends Endpoint { // eslint- private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - return Array.isArray(ps.userId) - ? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()]) - : await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]); + const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; + + const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id))); + + return Array.isArray(ps.userId) ? relations : relations[0]; }); } } diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 5ff6de37d2..8162cc1cb2 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,13 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; -import { AbuseReportService } from '@/core/AbuseReportService.js'; +import { QueueService } from '@/core/QueueService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -51,32 +54,39 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + + private idService: IdService, private getterService: GetterService, private roleService: RoleService, - private abuseReportService: AbuseReportService, + private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { // Lookup user - const targetUser = await this.getterService.getUser(ps.userId).catch(err => { + const user = await this.getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; }); - if (targetUser.id === me.id) { + if (user.id === me.id) { throw new ApiError(meta.errors.cannotReportYourself); } - if (await this.roleService.isAdministrator(targetUser)) { + if (await this.roleService.isAdministrator(user)) { throw new ApiError(meta.errors.cannotReportAdmin); } - await this.abuseReportService.report([{ - targetUserId: targetUser.id, - targetUserHost: targetUser.host, + const report = await this.abuseUserReportsRepository.insert({ + id: this.idService.gen(), + targetUserId: user.id, + targetUserHost: user.host, reporterId: me.id, reporterHost: null, comment: ps.comment, - }]); + }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); + + this.queueService.createReportAbuseJob(report); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 8ff952dcb5..1874b6d78d 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -1,11 +1,17 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository, FollowingsRepository } from '@/models/_.js'; +import type { Config } from '@/config.js'; +import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserSearchService } from '@/core/UserSearchService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['users'], @@ -43,16 +49,89 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - private userSearchService: UserSearchService, + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, ) { - super(meta, paramDef, (ps, me) => { - return this.userSearchService.search({ - username: ps.username, - host: ps.host, - }, { - limit: ps.limit, - detail: ps.detail, - }, me); + super(meta, paramDef, async (ps, me) => { + const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => { + if (ps.username) { + query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); + } + + if (ps.host) { + if (ps.host === this.config.hostname || ps.host === '.') { + query.andWhere('user.host IS NULL'); + } else { + query.andWhere('user.host LIKE :host', { + host: sqlLikeEscape(ps.host.toLowerCase()) + '%', + }); + } + } + + return query; + }; + + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + + let users: MiUser[] = []; + + if (me) { + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const query = setUsernameAndHostQuery() + .andWhere(`user.id IN (${ followingQuery.getQuery() })`) + .andWhere('user.id != :meId', { meId: me.id }) + .andWhere('user.isSuspended = FALSE') + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })); + + query.setParameters(followingQuery.getParameters()); + + users = await query + .orderBy('user.usernameLower', 'ASC') + .limit(ps.limit) + .getMany(); + + if (users.length < ps.limit) { + const otherQuery = setUsernameAndHostQuery() + .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`) + .andWhere('user.isSuspended = FALSE') + .andWhere('user.updatedAt IS NOT NULL'); + + otherQuery.setParameters(followingQuery.getParameters()); + + const otherUsers = await otherQuery + .orderBy('user.updatedAt', 'DESC') + .limit(ps.limit - users.length) + .getMany(); + + users = users.concat(otherUsers); + } + } else { + const query = setUsernameAndHostQuery() + .andWhere('user.isSuspended = FALSE') + .andWhere('user.updatedAt IS NOT NULL'); + + users = await query + .orderBy('user.updatedAt', 'DESC') + .limit(ps.limit - users.length) + .getMany(); + } + + return await this.userEntityService.packMany(users, me, { detail: !!ps.detail }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 0b0136066d..d597270e3a 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -57,69 +57,91 @@ export default class extends Endpoint { // eslint- const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 ps.query = ps.query.trim(); - const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1; + const isUsername = ps.query.startsWith('@'); let users: MiUser[] = []; - const nameQuery = this.usersRepository.createQueryBuilder('user') - .where(new Brackets(qb => { - qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); - - if (isUsername) { - qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }); - } else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username - qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); - } - })) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE'); - - if (ps.origin === 'local') { - nameQuery.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - nameQuery.andWhere('user.host IS NOT NULL'); - } - - users = await nameQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .limit(ps.limit) - .offset(ps.offset) - .getMany(); - - if (users.length < ps.limit) { - const profQuery = this.userProfilesRepository.createQueryBuilder('prof') - .select('prof.userId') - .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); + if (isUsername) { + const usernameQuery = this.usersRepository.createQueryBuilder('user') + .where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }) + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); if (ps.origin === 'local') { - profQuery.andWhere('prof.userHost IS NULL'); + usernameQuery.andWhere('user.host IS NULL'); } else if (ps.origin === 'remote') { - profQuery.andWhere('prof.userHost IS NOT NULL'); + usernameQuery.andWhere('user.host IS NOT NULL'); } - const query = this.usersRepository.createQueryBuilder('user') - .where(`user.id IN (${ profQuery.getQuery() })`) + users = await usernameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .limit(ps.limit) + .offset(ps.offset) + .getMany(); + } else { + const nameQuery = this.usersRepository.createQueryBuilder('user') + .where(new Brackets(qb => { + qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); + + // Also search username if it qualifies as username + if (this.userEntityService.validateLocalUsername(ps.query)) { + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); + } + })) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })) - .andWhere('user.isSuspended = FALSE') - .setParameters(profQuery.getParameters()); + .andWhere('user.isSuspended = FALSE'); + + if (ps.origin === 'local') { + nameQuery.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + nameQuery.andWhere('user.host IS NOT NULL'); + } - users = users.concat(await query + users = await nameQuery .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') .limit(ps.limit) .offset(ps.offset) - .getMany(), - ); + .getMany(); + + if (users.length < ps.limit) { + const profQuery = this.userProfilesRepository.createQueryBuilder('prof') + .select('prof.userId') + .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); + + if (ps.origin === 'local') { + profQuery.andWhere('prof.userHost IS NULL'); + } else if (ps.origin === 'remote') { + profQuery.andWhere('prof.userHost IS NOT NULL'); + } + + const query = this.usersRepository.createQueryBuilder('user') + .where(`user.id IN (${ profQuery.getQuery() })`) + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE') + .setParameters(profQuery.getParameters()); + + users = users.concat(await query + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .limit(ps.limit) + .offset(ps.offset) + .getMany(), + ); + } } - return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }); + return await this.userEntityService.packMany(users, me, { detail: ps.detail }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 07a62c46fa..f3b5dea4a6 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -110,16 +110,14 @@ export default class extends Endpoint { // eslint- }); // リクエストされた通りに並べ替え - // 順番は保持されるけど数は減ってる可能性がある const _users: MiUser[] = []; for (const id of ps.userIds) { - const user = users.find(x => x.id === id); - if (user != null) _users.push(user); + _users.push(users.find(x => x.id === id)!); } - const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) - .then(users => new Map(users.map(u => [u.id, u]))); - return _users.map(u => _userMap.get(u.id)!); + return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, { + detail: true, + }))); } else { // Lookup user if (typeof ps.host === 'string' && typeof ps.username === 'string') { @@ -148,7 +146,7 @@ export default class extends Endpoint { // eslint- } return await this.userEntityService.pack(user, me, { - schema: 'UserDetailed', + detail: true, }); } }); diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index e4175cf7ef..b15e4faa7c 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/endpoints/users/translate.ts b/packages/backend/src/server/api/endpoints/users/translate.ts index 8a6b7b14fe..2848711790 100644 --- a/packages/backend/src/server/api/endpoints/users/translate.ts +++ b/packages/backend/src/server/api/endpoints/users/translate.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -24,7 +24,7 @@ export const meta = { res: { type: 'object', - optional: true, nullable: false, + optional: false, nullable: false, properties: { sourceLang: { type: 'string' }, text: { type: 'string' }, @@ -79,7 +79,7 @@ export default class extends Endpoint { // eslint- }); if (target.description == null) { - return; + return 204; } const instance = await this.metaService.fetch(); @@ -91,7 +91,7 @@ export default class extends Endpoint { // eslint- ]; if (instance.translatorType == null || !translatorServices.includes(instance.translatorType)) { - return Promise.resolve(204); // Promise.resolveで204をラップする + throw new ApiError(meta.errors.noTranslateService); } let targetLang = ps.targetLang; @@ -100,7 +100,7 @@ export default class extends Endpoint { // eslint- let translationResult; if (instance.translatorType === 'deepl') { if (instance.deeplAuthKey == null) { - throw new ApiError(meta.errors.unavailable); + return 204; // TODO: 良い感じのエラー返す } translationResult = await this.translateDeepL(target.description, targetLang, instance.deeplAuthKey, instance.deeplIsPro, instance.translatorType); } else if (instance.translatorType === 'google_no_api') { @@ -112,12 +112,12 @@ export default class extends Endpoint { // eslint- return { sourceLang: raw.src, text: text, - translator: instance.translatorType, // 修正点: 配列ではなく単一の文字列 + translator: translatorServices, }; } else if (instance.translatorType === 'ctav3') { - if (instance.ctav3SaKey == null) return Promise.resolve(204); - else if (instance.ctav3ProjectId == null) return Promise.resolve(204); - else if (instance.ctav3Location == null) return Promise.resolve(204); + if (instance.ctav3SaKey == null) return 204; + else if (instance.ctav3ProjectId == null) return 204; + else if (instance.ctav3Location == null) return 204; translationResult = await this.apiCloudTranslationAdvanced( target.description, targetLang, instance.ctav3SaKey, instance.ctav3ProjectId, instance.ctav3Location, instance.ctav3Model, instance.ctav3Glossary, instance.translatorType, ); @@ -125,11 +125,11 @@ export default class extends Endpoint { // eslint- throw new Error('Unsupported translator type'); } - return Promise.resolve({ - sourceLang: translationResult.sourceLang || '', - text: translationResult.text || '', - translator: translationResult.translator || [], - }); + return { + sourceLang: translationResult.sourceLang, + text: translationResult.text, + translator: translationResult.translator, + }; }); } diff --git a/packages/backend/src/server/api/endpoints/users/update-memo.ts b/packages/backend/src/server/api/endpoints/users/update-memo.ts index 5a10de0c40..9e3a564b84 100644 --- a/packages/backend/src/server/api/endpoints/users/update-memo.ts +++ b/packages/backend/src/server/api/endpoints/users/update-memo.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 2f8322a568..a195724183 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts index f124aa9f39..c27e57c7a2 100644 --- a/packages/backend/src/server/api/openapi/OpenApiServerService.ts +++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -25,7 +25,7 @@ export class OpenApiServerService { public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.get('/api-doc', async (_request, reply) => { reply.header('Cache-Control', 'public, max-age=86400'); - return await reply.sendFile('/api-doc.html', staticAssets); + return await reply.sendFile('/redoc.html', staticAssets); }); fastify.get('/api.json', (_request, reply) => { reply.header('Cache-Control', 'public, max-age=600'); diff --git a/packages/backend/src/server/api/openapi/errors.ts b/packages/backend/src/server/api/openapi/errors.ts index ff19bf4d57..9457e6f965 100644 --- a/packages/backend/src/server/api/openapi/errors.ts +++ b/packages/backend/src/server/api/openapi/errors.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index d921100f8a..b623307a7e 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -1,21 +1,22 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import type { Config } from '@/config.js'; import endpoints, { IEndpoint } from '../endpoints.js'; import { errors as basicErrors } from './errors.js'; -import { getSchemas, convertSchemaToOpenApiSchema } from './schemas.js'; +import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; -export function genOpenapiSpec(config: Config, includeSelfRef = false) { +export function genOpenapiSpec(config: Config) { const spec = { - openapi: '3.1.0', + openapi: '3.0.0', info: { version: config.version, description: config.basedMisskeyVersion, title: 'CherryPick API', + 'x-logo': { url: '/static-assets/api-doc.png' }, }, externalDocs: { @@ -30,7 +31,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { paths: {} as any, components: { - schemas: getSchemas(includeSelfRef), + schemas: schemas, securitySchemes: { bearerAuth: { @@ -56,7 +57,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { } } - const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res', includeSelfRef) : {}; + const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; @@ -71,7 +72,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { } const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; - const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param', false) }; + const schema = { ...endpoint.params }; if (endpoint.meta.requireFile) { schema.properties = { @@ -93,7 +94,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1); const info = { - operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない + operationId: endpoint.name, summary: endpoint.name, description: desc, externalDocs: { @@ -210,9 +211,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { }; spec.paths['/' + endpoint.name] = { - ...(endpoint.meta.allowGet ? { - get: info, - } : {}), + ...(endpoint.meta.allowGet ? { get: info } : {}), post: info, }; } diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index eb854a7141..b1351407ce 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -1,40 +1,37 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ 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はスキーマ定義に含まれないので分離しておく +export function convertSchemaToOpenApiSchema(schema: Schema) { + // optional, refはスキーマ定義に含まれないので分離しておく // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { optional, nullable, ref, selfRef, ...res }: any = schema; + const { optional, ref, ...res }: any = schema; if (schema.type === 'object' && schema.properties) { - if (type === 'res') { - const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); - if (required.length > 0) { + const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); + if (required.length > 0) { // 空配列は許可されない - res.required = required; - } + res.required = required; } for (const k of Object.keys(schema.properties)) { - res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type, includeSelfRef); + res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); } } if (schema.type === 'array' && schema.items) { - res.items = convertSchemaToOpenApiSchema(schema.items, type, includeSelfRef); + res.items = convertSchemaToOpenApiSchema(schema.items); } - for (const o of ['anyOf', 'oneOf', 'allOf'] as const) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type, includeSelfRef)); - } + if (schema.anyOf) res.anyOf = schema.anyOf.map(convertSchemaToOpenApiSchema); + if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema); + if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema); - if (type === 'res' && schema.ref && (!schema.selfRef || includeSelfRef)) { + if (schema.ref) { const $ref = `#/components/schemas/${schema.ref}`; if (schema.nullable || schema.optional) { res.allOf = [{ $ref }]; @@ -43,48 +40,38 @@ export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 're } } - if (schema.nullable) { - if (Array.isArray(schema.type) && !schema.type.includes('null')) { - res.type.push('null'); - } else if (typeof schema.type === 'string') { - res.type = [res.type, 'null']; - } - } - return res; } -export function getSchemas(includeSelfRef: boolean) { - return { - Error: { - type: 'object', - properties: { - error: { - type: 'object', - description: 'An error object.', - properties: { - code: { - type: 'string', - description: 'An error code. Unique within the endpoint.', - }, - message: { - type: 'string', - description: 'An error message.', - }, - id: { - type: 'string', - format: 'uuid', - description: 'An error ID. This ID is static.', - }, +export const schemas = { + Error: { + type: 'object', + properties: { + error: { + type: 'object', + description: 'An error object.', + properties: { + code: { + type: 'string', + description: 'An error code. Unique within the endpoint.', + }, + message: { + type: 'string', + description: 'An error message.', + }, + id: { + type: 'string', + format: 'uuid', + description: 'An error ID. This ID is static.', }, - required: ['code', 'id', 'message'], }, + required: ['code', 'id', 'message'], }, - required: ['error'], }, + required: ['error'], + }, - ...Object.fromEntries( - Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res', includeSelfRef)]), - ), - }; -} + ...Object.fromEntries( + Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]), + ), +}; diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 5d734fb378..f3bfaa0faa 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -51,7 +51,6 @@ export class ChannelsService { case 'main': return this.mainChannelService; case 'homeTimeline': return this.homeTimelineChannelService; case 'localTimeline': return this.localTimelineChannelService; - case 'mediaTimeline': return this.globalTimelineChannelService; case 'hybridTimeline': return this.hybridTimelineChannelService; case 'globalTimeline': return this.globalTimelineChannelService; case 'userList': return this.userListChannelService; diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index c68d9ce1a9..979c28931e 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,7 +15,6 @@ import { MiFollowing, MiUserProfile } from '@/models/_.js'; import type { MiUserGroup } from '@/models/UserGroup.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; -import type { JsonObject } from '@/misc/json-value.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; import type Channel from './channel.js'; @@ -30,7 +29,7 @@ export default class Connection { private wsConnection: WebSocket.WebSocket; public subscriber: StreamEventEmitter; private channels: Channel[] = []; - private subscribingNotes: Partial> = {}; + private subscribingNotes: any = {}; private cachedNotes: Packed<'Note'>[] = []; public userProfile: MiUserProfile | null = null; public following: Record | undefined> = {}; @@ -103,7 +102,7 @@ export default class Connection { */ @bindThis private async onWsConnectionMessage(data: WebSocket.RawData) { - let obj: JsonObject; + let obj: Record; try { obj = JSON.parse(data.toString()); @@ -113,8 +112,6 @@ export default class Connection { const { type, body } = obj; - if (typeof body !== 'object' || body === null || Array.isArray(body)) return; - switch (type) { case 'readNotification': this.onReadNotification(body); break; case 'subNote': this.onSubscribeNote(body); break; @@ -160,7 +157,7 @@ export default class Connection { } @bindThis - private readNote(body: JsonObject) { + private readNote(body: any) { const id = body.id; const note = this.cachedNotes.find(n => n.id === id); @@ -172,7 +169,7 @@ export default class Connection { } @bindThis - private onReadNotification(payload: JsonObject) { + private onReadNotification(payload: any) { this.notificationService.readAllNotification(this.user!.id); } @@ -180,14 +177,16 @@ export default class Connection { * 投稿購読要求時 */ @bindThis - private onSubscribeNote(payload: JsonObject) { - if (!payload.id || typeof payload.id !== 'string') return; + private onSubscribeNote(payload: any) { + if (!payload.id) return; + + if (this.subscribingNotes[payload.id] == null) { + this.subscribingNotes[payload.id] = 0; + } - const current = this.subscribingNotes[payload.id] ?? 0; - const updated = current + 1; - this.subscribingNotes[payload.id] = updated; + this.subscribingNotes[payload.id]++; - if (updated === 1) { + if (this.subscribingNotes[payload.id] === 1) { this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); } } @@ -196,14 +195,11 @@ export default class Connection { * 投稿購読解除要求時 */ @bindThis - private onUnsubscribeNote(payload: JsonObject) { - if (!payload.id || typeof payload.id !== 'string') return; - - const current = this.subscribingNotes[payload.id]; - if (current == null) return; - const updated = current - 1; - this.subscribingNotes[payload.id] = updated; - if (updated <= 0) { + private onUnsubscribeNote(payload: any) { + if (!payload.id) return; + + this.subscribingNotes[payload.id]--; + if (this.subscribingNotes[payload.id] <= 0) { delete this.subscribingNotes[payload.id]; this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); } @@ -222,22 +218,17 @@ export default class Connection { * チャンネル接続要求時 */ @bindThis - private onChannelConnectRequested(payload: JsonObject) { + private onChannelConnectRequested(payload: any) { const { channel, id, params, pong } = payload; - if (typeof id !== 'string') return; - if (typeof channel !== 'string') return; - if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return; - if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return; - this.connectChannel(id, params, channel, pong ?? undefined); + this.connectChannel(id, params, channel, pong); } /** * チャンネル切断要求時 */ @bindThis - private onChannelDisconnectRequested(payload: JsonObject) { + private onChannelDisconnectRequested(payload: any) { const { id } = payload; - if (typeof id !== 'string') return; this.disconnectChannel(id); } @@ -245,7 +236,7 @@ export default class Connection { * クライアントにメッセージ送信 */ @bindThis - public sendMessageToWs(type: string, payload: JsonObject) { + public sendMessageToWs(type: string, payload: any) { this.wsConnection.send(JSON.stringify({ type: type, body: payload, @@ -256,7 +247,7 @@ export default class Connection { * チャンネルに接続 */ @bindThis - public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) { + public connectChannel(id: string, params: any, channel: string, pong = false) { const channelService = this.channelsService.getChannelService(channel); if (channelService.requireCredential && this.user == null) { @@ -303,11 +294,7 @@ export default class Connection { * @param data メッセージ */ @bindThis - private onChannelMessageRequested(data: JsonObject) { - if (typeof data.id !== 'string') return; - if (typeof data.type !== 'string') return; - if (typeof data.body === 'undefined') return; - + private onChannelMessageRequested(data: any) { const channel = this.channels.find(c => c.id === data.id); if (channel != null && channel.onMessage != null) { channel.onMessage(data.type, data.body); diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 84cb552369..64408add60 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -1,14 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { bindThis } from '@/decorators.js'; -import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { Packed } from '@/misc/json-schema.js'; -import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import type Connection from './Connection.js'; /** @@ -59,35 +54,15 @@ export default abstract class Channel { return this.connection.subscriber; } - /* - * ミュートとブロックされてるを処理する - */ - protected isNoteMutedOrBlocked(note: Packed<'Note'>): boolean { - // 流れてきたNoteがインスタンスミュートしたインスタンスが関わる - if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return true; - - // 流れてきたNoteがミュートしているユーザーが関わる - if (isUserRelated(note, this.userIdsWhoMeMuting)) return true; - // 流れてきたNoteがブロックされているユーザーが関わる - if (isUserRelated(note, this.userIdsWhoBlockingMe)) return true; - - // 流れてきたNoteがリノートをミュートしてるユーザが行ったもの - if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true; - - return false; - } - constructor(id: string, connection: Connection) { this.id = id; this.connection = connection; } - public send(payload: { type: string, body: JsonValue }): void - public send(type: string, payload: JsonValue): void @bindThis - public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) { - const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string); - const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload; + public send(typeOrPayload: any, payload?: any) { + const type = payload === undefined ? typeOrPayload.type : typeOrPayload; + const body = payload === undefined ? typeOrPayload.body : payload; this.connection.sendMessageToWs('channel', { id: this.id, @@ -96,11 +71,11 @@ export default abstract class Channel { }); } - public abstract init(params: JsonObject): void; + public abstract init(params: any): void; public dispose?(): void; - public onMessage?(type: string, body: JsonValue): void; + public onMessage?(type: string, body: any): void; } export type MiChannelService = { diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index 355d5dba21..84a64493d9 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -1,11 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class AdminChannel extends Channel { @@ -15,7 +14,7 @@ class AdminChannel extends Channel { public static kind = 'read:admin:stream'; @bindThis - public async init(params: JsonObject) { + public async init(params: any) { // Subscribe admin stream this.subscriber.on(`adminStream:${this.user!.id}`, data => { this.send(data); diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 53dc7f18b6..c3588f9f75 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -1,13 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class AntennaChannel extends Channel { @@ -28,9 +28,8 @@ class AntennaChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (typeof params.antennaId !== 'string') return; - this.antennaId = params.antennaId; + public async init(params: any) { + this.antennaId = params.antennaId as string; // Subscribe stream this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); @@ -41,7 +40,12 @@ class AntennaChannel extends Channel { if (data.type === 'note') { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); - if (this.isNoteMutedOrBlocked(note)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index c973635c87..272c5fa863 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -1,17 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { MiUser } from '@/models/User.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; import Channel, { type MiChannelService } from '../channel.js'; class ChannelChannel extends Channel { @@ -35,9 +34,8 @@ class ChannelChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (typeof params.channelId !== 'string') return; - this.channelId = params.channelId; + public async init(params: any) { + this.channelId = params.channelId as string; // Subscribe stream this.subscriber.on('notesStream', this.onNote); @@ -49,9 +47,14 @@ class ChannelChannel extends Channel { private async onNote(note: Packed<'Note'>) { if (note.channelId !== this.channelId) return; - if (this.isNoteMutedOrBlocked(note)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { + if (this.user && note.renoteId && !note.text) { 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; @@ -84,7 +87,7 @@ class ChannelChannel extends Channel { if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; } - const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { schema: 'UserLite' }); + const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false }); this.send({ type: 'typers', diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index 03768f3d23..09607cb436 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -1,11 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class DriveChannel extends Channel { @@ -15,7 +14,7 @@ class DriveChannel extends Channel { public static kind = 'read:account'; @bindThis - public async init(params: JsonObject) { + public async init(params: any) { // Subscribe drive stream this.subscriber.on(`driveStream:${this.user!.id}`, data => { this.send(data); 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 92f7b2c09b..351c8a4956 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,16 +1,17 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class GlobalTimelineChannel extends Channel { @@ -19,7 +20,6 @@ class GlobalTimelineChannel extends Channel { public static requireCredential = false as const; private withRenotes: boolean; private withFiles: boolean; - private withoutBots: boolean; constructor( private metaService: MetaService, @@ -34,13 +34,12 @@ class GlobalTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.gtlAvailable) return; - this.withRenotes = !!(params.withRenotes ?? true); - this.withFiles = !!(params.withFiles ?? false); - this.withoutBots = !!(params.withoutBots ?? false); + this.withRenotes = params.withRenotes ?? true; + this.withFiles = params.withFiles ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -49,16 +48,30 @@ class GlobalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (this.withoutBots && note.user.isBot) return; if (note.visibility !== 'public') return; if (note.channelId != null) return; - if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; + // 関係ない返信は除外 + if (note.reply && !this.following[note.userId]?.withReplies) { + const reply = note.reply; + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; + } + + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (this.isNoteMutedOrBlocked(note)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { + if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 8105f15cb1..529fb377e8 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -1,15 +1,14 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class HashtagChannel extends Channel { @@ -29,11 +28,11 @@ class HashtagChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (!Array.isArray(params.q)) return; - if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return; + public async init(params: any) { this.q = params.q; + if (this.q == null) return; + // Subscribe stream this.subscriber.on('notesStream', this.onNote); } @@ -44,9 +43,14 @@ class HashtagChannel extends Channel { const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag)))); if (!matched) return; - if (this.isNoteMutedOrBlocked(note)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { + if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 591a45a6f5..880d8ab259 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -1,14 +1,15 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class HomeTimelineChannel extends Channel { @@ -18,7 +19,6 @@ class HomeTimelineChannel extends Channel { public static kind = 'read:account'; private withRenotes: boolean; private withFiles: boolean; - private withoutBots: boolean; constructor( private noteEntityService: NoteEntityService, @@ -31,10 +31,9 @@ class HomeTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - this.withRenotes = !!(params.withRenotes ?? true); - this.withFiles = !!(params.withFiles ?? false); - this.withoutBots = !!(params.withoutBots ?? false); + public async init(params: any) { + this.withRenotes = params.withRenotes ?? true; + this.withFiles = params.withFiles ?? false; this.subscriber.on('notesStream', this.onNote); } @@ -44,7 +43,6 @@ class HomeTimelineChannel extends Channel { const isMe = this.user!.id === note.userId; if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (this.withoutBots && note.user.isBot) return; if (note.channelId) { if (!this.followingChannels.has(note.channelId)) return; @@ -53,6 +51,9 @@ class HomeTimelineChannel extends Channel { if (!isMe && !Object.hasOwn(this.following, note.userId)) return; } + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances))) return; + if (note.visibility === 'followers') { if (!isMe && !Object.hasOwn(this.following, note.userId)) return; } else if (note.visibility === 'specified') { @@ -63,26 +64,23 @@ class HomeTimelineChannel extends Channel { const reply = note.reply; if (this.following[note.userId]?.withReplies) { // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; } else { // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; } } - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; - } - } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (this.isNoteMutedOrBlocked(note)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { + if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 69985ce4a1..954134c301 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,16 +1,17 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class HybridTimelineChannel extends Channel { @@ -21,7 +22,6 @@ class HybridTimelineChannel extends Channel { private withRenotes: boolean; private withReplies: boolean; private withFiles: boolean; - private withoutBots: boolean; constructor( private metaService: MetaService, @@ -36,14 +36,13 @@ class HybridTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject): Promise { + public async init(params: any): Promise { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withRenotes = !!(params.withRenotes ?? true); - this.withReplies = !!(params.withReplies ?? false); - this.withFiles = !!(params.withFiles ?? false); - this.withoutBots = !!(params.withoutBots ?? false); + this.withRenotes = params.withRenotes ?? true; + this.withReplies = params.withReplies ?? false; + this.withFiles = params.withFiles ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -54,7 +53,6 @@ class HybridTimelineChannel extends Channel { const isMe = this.user!.id === note.userId; if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (this.withoutBots && note.user.isBot) return; // チャンネルの投稿ではなく、自分自身の投稿 または // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または @@ -73,28 +71,28 @@ class HybridTimelineChannel extends Channel { if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return; } - if (this.isNoteMutedOrBlocked(note)) return; + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances))) return; if (note.reply) { const reply = note.reply; if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) { // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; } else { // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; } } - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; - } - } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 1d16aefd11..dc5a267236 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,16 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class LocalTimelineChannel extends Channel { @@ -20,7 +20,6 @@ class LocalTimelineChannel extends Channel { private withRenotes: boolean; private withReplies: boolean; private withFiles: boolean; - private withoutBots: boolean; constructor( private metaService: MetaService, @@ -35,14 +34,13 @@ class LocalTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withRenotes = !!(params.withRenotes ?? true); - this.withReplies = !!(params.withReplies ?? false); - this.withFiles = !!(params.withFiles ?? false); - this.withoutBots = !!(params.withoutBots ?? false); + this.withRenotes = params.withRenotes ?? true; + this.withReplies = params.withReplies ?? false; + this.withFiles = params.withFiles ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -51,7 +49,6 @@ class LocalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (this.withoutBots && note.user.isBot) return; if (note.user.host !== null) return; if (note.visibility !== 'public') return; @@ -64,11 +61,16 @@ class LocalTimelineChannel extends Channel { if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; } - if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; - if (this.isNoteMutedOrBlocked(note)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; + + if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 863d7f4c4e..83de100a5e 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,6 @@ import { Injectable } from '@nestjs/common'; import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class MainChannel extends Channel { @@ -26,7 +25,7 @@ class MainChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { // Subscribe main stream channel this.subscriber.on(`mainStream:${this.user!.id}`, async data => { switch (data.type) { diff --git a/packages/backend/src/server/api/stream/channels/messaging-index.ts b/packages/backend/src/server/api/stream/channels/messaging-index.ts index 970c67c9f9..7f26b37353 100644 --- a/packages/backend/src/server/api/stream/channels/messaging-index.ts +++ b/packages/backend/src/server/api/stream/channels/messaging-index.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -25,9 +25,10 @@ class MessagingIndexChannel extends Channel { export class MessagingIndexChannelService { public readonly shouldShare = MessagingIndexChannel.shouldShare; public readonly requireCredential = MessagingIndexChannel.requireCredential; - public readonly kind: string = 'messagingIndex'; // kind の型を string に変更し、適切な値を代入する - constructor() {} + constructor( + ) { + } @bindThis public create(id: string, connection: Channel['connection']): MessagingIndexChannel { diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts index 7c7cb3b253..e198aec4e6 100644 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ b/packages/backend/src/server/api/stream/channels/messaging.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project + * SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -113,7 +113,7 @@ class MessagingChannel extends Channel { if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; } - const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { schema: 'UserLite' }); + const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false }); this.send({ type: 'typers', @@ -133,7 +133,6 @@ class MessagingChannel extends Channel { export class MessagingChannelService { public readonly shouldShare = MessagingChannel.shouldShare; public readonly requireCredential = MessagingChannel.requireCredential; - public readonly kind: string = 'messaging'; // kind の型を string に変更し、適切な値に設定する constructor( @Inject(DI.usersRepository) 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 ff7e740226..82e3e61bc4 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -1,12 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; const ev = new Xev(); @@ -23,22 +22,19 @@ class QueueStatsChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { ev.addListener('queueStats', this.onStats); } @bindThis - private onStats(stats: JsonObject) { + private onStats(stats: any) { this.send('stats', stats); } @bindThis - public onMessage(type: string, body: JsonValue) { + public onMessage(type: string, body: any) { switch (type) { case 'requestLog': - if (typeof body !== 'object' || body === null || Array.isArray(body)) return; - if (typeof body.id !== 'string') return; - if (typeof body.length !== 'number') return; ev.once(`queueStatsLog:${body.id}`, statsLog => { this.send('statsLog', statsLog); }); 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..4364f8b951 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -1,14 +1,15 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Injectable } from '@nestjs/common'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class RoleTimelineChannel extends Channel { @@ -29,9 +30,8 @@ class RoleTimelineChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (typeof params.roleId !== 'string') return; - this.roleId = params.roleId; + public async init(params: any) { + this.roleId = params.roleId as string; this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent); } @@ -46,7 +46,12 @@ class RoleTimelineChannel extends Channel { } if (note.visibility !== 'public') return; - if (this.isNoteMutedOrBlocked(note)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.send('note', note); } else { 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 6258afba35..75516d6de3 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -1,12 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; const ev = new Xev(); @@ -23,20 +22,19 @@ class ServerStatsChannel extends Channel { } @bindThis - public async init(params: JsonObject) { + public async init(params: any) { ev.addListener('serverStats', this.onStats); } @bindThis - private onStats(stats: JsonObject) { + private onStats(stats: any) { this.send('stats', stats); } @bindThis - public onMessage(type: string, body: JsonValue) { + public onMessage(type: string, body: any) { switch (type) { case 'requestLog': - if (typeof body !== 'object' || body === null || Array.isArray(body)) return; ev.once(`serverStatsLog:${body.id}`, statsLog => { this.send('statsLog', statsLog); }); 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..bfaf645480 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,16 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; -import type { JsonObject } from '@/misc/json-value.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import Channel, { type MiChannelService } from '../channel.js'; class UserListChannel extends Channel { @@ -21,7 +21,6 @@ class UserListChannel extends Channel { private membershipsMap: Record | undefined> = {}; private listUsersClock: NodeJS.Timeout; private withFiles: boolean; - private withRenotes: boolean; constructor( private userListsRepository: UserListsRepository, @@ -37,14 +36,12 @@ class UserListChannel extends Channel { } @bindThis - public async init(params: JsonObject) { - if (typeof params.listId !== 'string') return; - this.listId = params.listId; - this.withFiles = !!(params.withFiles ?? false); - this.withRenotes = !!(params.withRenotes ?? true); + public async init(params: any) { + this.listId = params.listId as string; + this.withFiles = params.withFiles ?? false; // Check existence and owner - const listExist = await this.userListsRepository.exists({ + const listExist = await this.userListsRepository.exist({ where: { id: this.listId, userId: this.user!.id, @@ -107,17 +104,23 @@ class UserListChannel extends Channel { } } - if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - if (this.isNoteMutedOrBlocked(note)) return; + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { + if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); note.renote.myReaction = myRenoteReaction; } } + // 流れてきたNoteがミュートしているインスタンスに関わるものだったら無視する + if (isInstanceMuted(note, this.userMutedInstances)) return; + this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index 2108be25c3..0ae0d5f9af 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,7 +13,6 @@ import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, O import oauth2Pkce from 'oauth2orize-pkce'; import fastifyCors from '@fastify/cors'; import fastifyView from '@fastify/view'; -import rateLimit from '@fastify/rate-limit'; import pug from 'pug'; import bodyParser from 'body-parser'; import fastifyExpress from '@fastify/express'; @@ -394,12 +393,6 @@ export class OAuth2ProviderService { }, }); - - await fastify.register(rateLimit, { - max: 100, - timeWindow: '1 hour' - }); - await fastify.register(fastifyExpress); fastify.use('/authorize', this.#server.authorize(((areq, done) => { (async (): Promise> => { diff --git a/packages/backend/src/server/web/ClientLoggerService.ts b/packages/backend/src/server/web/ClientLoggerService.ts index 83d8b5bc38..a83b734109 100644 --- a/packages/backend/src/server/web/ClientLoggerService.ts +++ b/packages/backend/src/server/web/ClientLoggerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index ceed7adcb8..8744f9372e 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -19,33 +19,21 @@ import fastifyView from '@fastify/view'; import fastifyCookie from '@fastify/cookie'; import fastifyProxy from '@fastify/http-proxy'; import vary from 'vary'; -import htmlSafeJsonStringify from 'htmlescape'; import type { Config } from '@/config.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import { DI } from '@/di-symbols.js'; import * as Acct from '@/misc/acct.js'; import { MetaService } from '@/core/MetaService.js'; -import type { - DbQueue, - DeliverQueue, - EndedPollNotificationQueue, - InboxQueue, - ObjectStorageQueue, - SystemQueue, - UserWebhookDeliverQueue, - SystemWebhookDeliverQueue, - ScheduledNoteDeleteQueue, -} from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import { MetaEntityService } from '@/core/entities/MetaEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; -import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; +import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { RoleService } from '@/core/RoleService.js'; @@ -62,7 +50,6 @@ const clientAssets = `${_dirname}/../../../../frontend/assets/`; const assets = `${_dirname}/../../../../../built/_frontend_dist_/`; const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; const viteOut = `${_dirname}/../../../../../built/_vite_/`; -const tarball = `${_dirname}/../../../../../built/tarball/`; @Injectable() export class ClientServerService { @@ -100,7 +87,6 @@ export class ClientServerService { private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private pageEntityService: PageEntityService, - private metaEntityService: MetaEntityService, private galleryPostEntityService: GalleryPostEntityService, private clipEntityService: ClipEntityService, private channelEntityService: ChannelEntityService, @@ -112,13 +98,11 @@ export class ClientServerService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, - @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, - @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, ) { //this.createServer = this.createServer.bind(this); } @@ -182,7 +166,7 @@ export class ClientServerService { } @bindThis - private async generateCommonPugData(meta: MiMeta) { + private generateCommonPugData(meta: MiMeta) { return { instanceName: meta.name ?? 'CherryPick', icon: meta.iconUrl, @@ -192,8 +176,6 @@ export class ClientServerService { infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', instanceUrl: this.config.url, - metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)), - now: Date.now(), }; } @@ -206,18 +188,9 @@ export class ClientServerService { // Authenticate fastify.addHook('onRequest', async (request, reply) => { - if (request.routeOptions.url == null) { - reply.code(404).send('Not found'); - return; - } - // %71ueueとかでリクエストされたら困るため const url = decodeURI(request.routeOptions.url); if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) { - if (!url.startsWith(bullBoardPath + '/static/')) { - reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); - } - const token = request.cookies.token; if (token == null) { reply.code(401).send('Login required'); @@ -242,13 +215,11 @@ export class ClientServerService { queues: [ this.systemQueue, this.endedPollNotificationQueue, - this.scheduledNoteDeleteQueue, this.deliverQueue, this.inboxQueue, this.dbQueue, this.objectStorageQueue, - this.userWebhookDeliverQueue, - this.systemWebhookDeliverQueue, + this.webhookDeliverQueue, ].map(q => new BullMQAdapter(q)), serverAdapter, }); @@ -277,16 +248,11 @@ export class ClientServerService { //#region vite assets if (this.config.clientManifestExists) { - fastify.register((fastify, options, done) => { - fastify.register(fastifyStatic, { - root: viteOut, - prefix: '/vite/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - done(); + fastify.register(fastifyStatic, { + root: viteOut, + prefix: '/vite/', + maxAge: ms('30 days'), + decorateReply: false, }); } else { const port = (process.env.VITE_PORT ?? '5173'); @@ -321,18 +287,6 @@ export class ClientServerService { decorateReply: false, }); - fastify.register((fastify, options, done) => { - fastify.register(fastifyStatic, { - root: tarball, - prefix: '/tarball/', - maxAge: ms('30 days'), - immutable: true, - decorateReply: false, - }); - fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); - done(); - }); - fastify.get('/favicon.ico', async (request, reply) => { return reply.sendFile('/favicon.ico', staticAssets); }); @@ -448,7 +402,7 @@ export class ClientServerService { //#endregion - const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => { + const renderBase = async (reply: FastifyReply) => { const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=30'); return await reply.view('base', { @@ -456,8 +410,7 @@ export class ClientServerService { url: this.config.url, title: meta.name ?? 'CherryPick', desc: meta.description, - ...await this.generateCommonPugData(meta), - ...data, + ...this.generateCommonPugData(meta), }); }; @@ -476,9 +429,7 @@ export class ClientServerService { }; // Atom - fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); - + fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => { const feed = await getFeed(request.params.user); if (feed) { @@ -491,9 +442,7 @@ export class ClientServerService { }); // RSS - fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); - + fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => { const feed = await getFeed(request.params.user); if (feed) { @@ -506,9 +455,7 @@ export class ClientServerService { }); // JSON - fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => { - if (request.params.user == null) return await renderBase(reply); - + fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => { const feed = await getFeed(request.params.user); if (feed) { @@ -530,8 +477,6 @@ export class ClientServerService { isSuspended: false, }); - vary(reply.raw, 'Accept'); - if (user != null) { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const meta = await this.metaService.fetch(); @@ -550,7 +495,7 @@ export class ClientServerService { user, profile, me, avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), sub: request.params.sub, - ...await this.generateCommonPugData(meta), + ...this.generateCommonPugData(meta), }); } else { // リモートユーザーなので @@ -571,8 +516,6 @@ export class ClientServerService { return; } - vary(reply.raw, 'Accept'); - reply.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`); }); @@ -600,7 +543,7 @@ export class ClientServerService { avatarUrl: _note.user.avatarUrl, // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), - ...await this.generateCommonPugData(meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -639,7 +582,7 @@ export class ClientServerService { page: _page, profile, avatarUrl: _page.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -665,7 +608,7 @@ export class ClientServerService { flash: _flash, profile, avatarUrl: _flash.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -691,7 +634,7 @@ export class ClientServerService { clip: _clip, profile, avatarUrl: _clip.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -715,7 +658,7 @@ export class ClientServerService { post: _post, profile, avatarUrl: _post.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -734,24 +677,13 @@ export class ClientServerService { reply.header('Cache-Control', 'public, max-age=15'); return await reply.view('channel', { channel: _channel, - ...await this.generateCommonPugData(meta), + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); } }); - - //region noindex pages - // Tags - fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => { - return await renderBase(reply, { noindex: true }); - }); - - // User with Tags - fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => { - return await renderBase(reply, { noindex: true }); - }); - //endregion + //#endregion fastify.get('/_info_card_', async (request, reply) => { const meta = await this.metaService.fetch(true); diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index d0b41f6c55..cdb67bb7e7 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,8 +14,6 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; -import { MfmService } from "@/core/MfmService.js"; -import { parse as mfmParse } from 'cherrypick-mfm-js'; @Injectable() export class FeedService { @@ -35,7 +33,6 @@ export class FeedService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private idService: IdService, - private mfmService: MfmService, ) { } @@ -79,14 +76,13 @@ export class FeedService { id: In(note.fileIds), }) : []; const file = files.find(file => file.type.startsWith('image/')); - const text = note.text; feed.addItem({ title: `New note by ${author.name}`, link: `${this.config.url}/notes/${note.id}`, date: this.idService.parse(note.id).date, description: note.cw ?? undefined, - content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined, + content: note.text ?? undefined, image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined, }); } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 8f8f08a305..f2c2aeff99 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,11 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; -import { summaly } from '@misskey-dev/summaly'; -import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; +import { summaly } from 'summaly'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; @@ -15,7 +14,6 @@ import { query } from '@/misc/prelude/url.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; -import { MiMeta } from '@/models/Meta.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -64,25 +62,24 @@ export class UrlPreviewService { const meta = await this.metaService.fetch(); - if (!meta.urlPreviewEnabled) { - reply.code(403); - return { - error: new ApiError({ - message: 'URL preview is disabled', - code: 'URL_PREVIEW_DISABLED', - id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8', - }), - }; - } - - this.logger.info(meta.urlPreviewSummaryProxyUrl + this.logger.info(meta.summalyProxy ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); - try { - const summary = meta.urlPreviewSummaryProxyUrl - ? await this.fetchSummaryFromProxy(url, meta, lang) - : await this.fetchSummary(url, meta, lang); + const summary = meta.summalyProxy ? + await this.httpRequestService.getJson>(`${meta.summalyProxy}?${query({ + url: url, + lang: lang ?? 'ja-JP', + })}`) + : + await summaly(url, { + followRedirects: false, + lang: lang ?? 'ja-JP', + agent: this.config.proxy ? { + http: this.httpRequestService.httpAgent, + https: this.httpRequestService.httpsAgent, + } : undefined, + }); this.logger.succ(`Got preview of ${url}: ${summary.title}`); @@ -103,7 +100,6 @@ export class UrlPreviewService { return summary; } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); - reply.code(422); reply.header('Cache-Control', 'max-age=86400, immutable'); return { @@ -115,37 +111,4 @@ export class UrlPreviewService { }; } } - - private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise { - const agent = this.config.proxy - ? { - http: this.httpRequestService.httpAgent, - https: this.httpRequestService.httpsAgent, - } - : undefined; - - return summaly(url, { - followRedirects: false, - lang: lang ?? 'ja-JP', - agent: agent, - userAgent: meta.urlPreviewUserAgent ?? undefined, - operationTimeout: meta.urlPreviewTimeout, - contentLengthLimit: meta.urlPreviewMaximumContentLength, - contentLengthRequired: meta.urlPreviewRequireContentLength, - }); - } - - private fetchSummaryFromProxy(url: string, meta: MiMeta, lang?: string): Promise { - const proxy = meta.urlPreviewSummaryProxyUrl!; - const queryStr = query({ - url: url, - lang: lang ?? 'ja-JP', - userAgent: meta.urlPreviewUserAgent ?? undefined, - operationTimeout: meta.urlPreviewTimeout, - contentLengthLimit: meta.urlPreviewMaximumContentLength, - contentLengthRequired: meta.urlPreviewRequireContentLength, - }); - - return this.httpRequestService.getJson(`${proxy}?${queryStr}`); - } } diff --git a/packages/backend/src/server/web/bios.css b/packages/backend/src/server/web/bios.css index 88cc5aef27..ae265c4820 100644 --- a/packages/backend/src/server/web/bios.css +++ b/packages/backend/src/server/web/bios.css @@ -1,12 +1,41 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * * SPDX-License-Identifier: AGPL-3.0-only */ -@import url("https://cdn.jsdelivr.net/npm/jetbrains-mono@1.0.6/css/jetbrains-mono.min.css"); -@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"); -@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-jp.min.css"); +@font-face { + font-family: 'Pretendard JP'; + font-weight: 400; + font-display: swap; + src: local('Pretendard JP Regular'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff2/PretendardJP-Regular.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff/PretendardJP-Regular.woff') format('woff'); +} +@font-face { + font-family: 'Pretendard JP'; + font-weight: 700; + font-display: swap; + src: local('Pretendard JP Bold'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff2/PretendardJP-Bold.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff/PretendardJP-Bold.woff') format('woff'); +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + src: local("JetBrains Mono Regular"), local("JetBrainsMono-Regular"), + url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono@master/fonts/webfonts/JetBrainsMono-Regular.woff2") format("woff2"); + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + src: local("JetBrains Mono Bold"), local("JetBrainsMono-Bold"), + url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono@master/fonts/webfonts/JetBrainsMono-Bold.woff2") format("woff2"); + font-display: swap; +} * { font-family: "JetBrains Mono", "Pretendard JP", Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; diff --git a/packages/backend/src/server/web/bios.js b/packages/backend/src/server/web/bios.js index 9ff5dca72a..343ae50e28 100644 --- a/packages/backend/src/server/web/bios.js +++ b/packages/backend/src/server/web/bios.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 5d5091c135..ef8845c56a 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -29,8 +29,7 @@ let forceError = localStorage.getItem('forceError'); if (forceError != null) { - renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); - return; + renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.') } //#region Detect language & fetch translations @@ -92,8 +91,8 @@ //#endregion //#region Script - async function importAppScript() { - await import(`/vite/${CLIENT_ENTRY}`) + function importAppScript() { + import(`/vite/${CLIENT_ENTRY}`) .catch(async e => { console.error(e); renderError('APP_IMPORT', e); @@ -166,12 +165,7 @@ document.head.appendChild(css); } - async function renderError(code, details) { - // Cannot set property 'innerHTML' of null を回避 - if (document.readyState === 'loading') { - await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); - } - + function renderError(code, details) { let errorsElement = document.getElementById('errors'); if (!errorsElement) { @@ -334,6 +328,7 @@ #errorInfo { width: 50%; } - }`) + } + `) } })(); diff --git a/packages/backend/src/server/web/cli.css b/packages/backend/src/server/web/cli.css index dec2365e1c..041c7d9755 100644 --- a/packages/backend/src/server/web/cli.css +++ b/packages/backend/src/server/web/cli.css @@ -1,12 +1,41 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * * SPDX-License-Identifier: AGPL-3.0-only */ -@import url("https://cdn.jsdelivr.net/npm/jetbrains-mono@1.0.6/css/jetbrains-mono.min.css"); -@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"); -@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-jp.min.css"); +@font-face { + font-family: 'Pretendard JP'; + font-weight: 400; + font-display: swap; + src: local('Pretendard JP Regular'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff2/PretendardJP-Regular.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff/PretendardJP-Regular.woff') format('woff'); +} +@font-face { + font-family: 'Pretendard JP'; + font-weight: 700; + font-display: swap; + src: local('Pretendard JP Bold'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff2/PretendardJP-Bold.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff/PretendardJP-Bold.woff') format('woff'); +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + src: local("JetBrains Mono Regular"), local("JetBrainsMono-Regular"), + url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono@master/fonts/webfonts/JetBrainsMono-Regular.woff2") format("woff2"); + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + src: local("JetBrains Mono Bold"), local("JetBrainsMono-Bold"), + url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono@master/fonts/webfonts/JetBrainsMono-Bold.woff2") format("woff2"); + font-display: swap; +} * { font-family: "JetBrains Mono", "Pretendard JP", Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js index 72541a6732..f66e63931e 100644 --- a/packages/backend/src/server/web/cli.js +++ b/packages/backend/src/server/web/cli.js @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/web/error.css b/packages/backend/src/server/web/error.css index 046faf7e52..a5943dfc79 100644 --- a/packages/backend/src/server/web/error.css +++ b/packages/backend/src/server/web/error.css @@ -1,12 +1,41 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * * SPDX-License-Identifier: AGPL-3.0-only */ -@import url("https://cdn.jsdelivr.net/npm/jetbrains-mono@1.0.6/css/jetbrains-mono.min.css"); -@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"); -@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-jp.min.css"); +@font-face { + font-family: 'Pretendard JP'; + font-weight: 400; + font-display: swap; + src: local('Pretendard JP Regular'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff2/PretendardJP-Regular.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff/PretendardJP-Regular.woff') format('woff'); +} +@font-face { + font-family: 'Pretendard JP'; + font-weight: 700; + font-display: swap; + src: local('Pretendard JP Bold'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff2/PretendardJP-Bold.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/packages/pretendard-jp/dist/web/static/woff/PretendardJP-Bold.woff') format('woff'); +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + src: local("JetBrains Mono Regular"), local("JetBrainsMono-Regular"), + url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono@master/fonts/webfonts/JetBrainsMono-Regular.woff2") format("woff2"); + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + src: local("JetBrains Mono Bold"), local("JetBrainsMono-Bold"), + url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono@master/fonts/webfonts/JetBrainsMono-Bold.woff2") format("woff2"); + font-display: swap; +} * { font-family: "Pretendard JP", "JetBrains Mono", BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index e1ba956168..90e68c9666 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 2b2623cb6d..1423d39b8a 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -36,7 +36,7 @@ html link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=notFoundImageUrl) //- https://github.com/misskey-dev/misskey/issues/9842 - link(rel='stylesheet' href=`/assets/tabler-icons.${version}/tabler-icons.min.css?v3.3.0`) + link(rel='stylesheet' href=`/assets/tabler-icons.${version}/tabler-icons.min.css`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists @@ -50,9 +50,6 @@ html block title = title || 'CherryPick' - if noindex - meta(name='robots' content='noindex') - block desc meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') @@ -71,9 +68,6 @@ html var VERSION = "#{version}"; var CLIENT_ENTRY = "#{clientEntry.file}"; - script(type='application/json' id='misskey_meta' data-generated-at=now) - != metaJson - script include ../boot.js diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index fb659ce171..9bc652b6a1 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -2,7 +2,7 @@ extends ./base block vars - const user = note.user; - - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`; + - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const url = `${config.url}/notes/${note.id}`; - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; - const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive) @@ -28,7 +28,7 @@ block og // FIXME: add embed player for Twitter if images.length meta(property='twitter:card' content='summary_large_image') - each image in images + each image in images meta(property='og:image' content= image.url) else meta(property='twitter:card' content='summary') diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug index 03c50eca8a..08bb08ffe7 100644 --- a/packages/backend/src/server/web/views/page.pug +++ b/packages/backend/src/server/web/views/page.pug @@ -3,7 +3,7 @@ extends ./base block vars - const user = page.user; - const title = page.title; - - const url = `${config.url}/@${user.username}/pages/${page.name}`; + - const url = `${config.url}/@${user.username}/${page.name}`; block title = `${title} | ${instanceName}` diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug index 2b0a7bab5c..83d57349a6 100644 --- a/packages/backend/src/server/web/views/user.pug +++ b/packages/backend/src/server/web/views/user.pug @@ -1,7 +1,7 @@ extends ./base block vars - - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`; + - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`; block title diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index c02a3cfb3d..6e0fe4e5f9 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -14,8 +14,8 @@ * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された - * groupInvited - グループに招待された * roleAssigned - ロールが付与された + * groupInvited - グループに招待された * achievementEarned - 実績を獲得 * app - アプリ通知 * test - テスト通知(サーバー側) @@ -31,22 +31,14 @@ export const notificationTypes = [ 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', - 'groupInvited', 'roleAssigned', + 'groupInvited', 'achievementEarned', 'app', - 'test', -] as const; - -export const groupedNotificationTypes = [ - ...notificationTypes, - 'reaction:grouped', - 'renote:grouped', -] as const; - + 'test'] as const; export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const; -export const noteVisibilities = ['public', 'home', 'followers', 'specified', 'private'] as const; +export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; @@ -57,8 +49,6 @@ export const moderationLogTypes = [ 'updateServerSettings', 'suspend', 'unsuspend', - 'setSensitive', - 'unsetSensitive', 'updateUserNote', 'addCustomEmoji', 'updateCustomEmoji', @@ -81,7 +71,6 @@ export const moderationLogTypes = [ 'resetPassword', 'suspendRemoteInstance', 'unsuspendRemoteInstance', - 'updateRemoteInstanceNote', 'markSensitiveDriveFile', 'unmarkSensitiveDriveFile', 'resolveAbuseReport', @@ -94,12 +83,6 @@ export const moderationLogTypes = [ 'deleteAvatarDecoration', 'unsetUserAvatar', 'unsetUserBanner', - 'createSystemWebhook', - 'updateSystemWebhook', - 'deleteSystemWebhook', - 'createAbuseReportNotificationRecipient', - 'updateAbuseReportNotificationRecipient', - 'deleteAbuseReportNotificationRecipient', ] as const; export type ModerationLogPayloads = { @@ -117,16 +100,6 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; - setSensitive: { - userId: string; - userUsername: string; - userHost: string | null; - }; - unsetSensitive: { - userId: string; - userUsername: string; - userHost: string | null; - }; updateUserNote: { userId: string; userUsername: string; @@ -238,12 +211,6 @@ export type ModerationLogPayloads = { id: string; host: string; }; - updateRemoteInstanceNote: { - id: string; - host: string; - before: string | null; - after: string | null; - }; markSensitiveDriveFile: { fileId: string; fileUserId: string | null; @@ -302,32 +269,6 @@ export type ModerationLogPayloads = { userHost: string | null; fileId: string; }; - createSystemWebhook: { - systemWebhookId: string; - webhook: any; - }; - updateSystemWebhook: { - systemWebhookId: string; - before: any; - after: any; - }; - deleteSystemWebhook: { - systemWebhookId: string; - webhook: any; - }; - createAbuseReportNotificationRecipient: { - recipientId: string; - recipient: any; - }; - updateAbuseReportNotificationRecipient: { - recipientId: string; - before: any; - after: any; - }; - deleteAbuseReportNotificationRecipient: { - recipientId: string; - recipient: any; - }; }; export type Serialized = { @@ -338,11 +279,7 @@ export type Serialized = { ? (string | null) : T[K] extends Record ? Serialized - : T[K] extends (Record | null) - ? (Serialized | null) - : T[K] extends (Record | undefined) - ? (Serialized | undefined) - : T[K]; + : T[K]; }; export type FilterUnionByProperty< diff --git a/packages/backend/test-server/.swcrc b/packages/backend/test-server/.swcrc deleted file mode 100644 index e3d6935169..0000000000 --- a/packages/backend/test-server/.swcrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/swcrc", - "jsc": { - "parser": { - "syntax": "typescript", - "dynamicImport": true, - "decorators": true - }, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, - "experimental": { - "keepImportAssertions": true - }, - "baseUrl": "../built", - "paths": { - "@/*": ["*"] - }, - "target": "es2022" - }, - "minify": false -} diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts deleted file mode 100644 index 866a7e1f5b..0000000000 --- a/packages/backend/test-server/entry.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { portToPid } from 'pid-port'; -import fkill from 'fkill'; -import Fastify from 'fastify'; -import { NestFactory } from '@nestjs/core'; -import { MainModule } from '@/MainModule.js'; -import { ServerService } from '@/server/ServerService.js'; -import { loadConfig } from '@/config.js'; -import { NestLogger } from '@/NestLogger.js'; - -const config = loadConfig(); -const originEnv = JSON.stringify(process.env); - -process.env.NODE_ENV = 'test'; - -/** - * テスト用のサーバインスタンスを起動する - */ -async function launch() { - await killTestServer(); - - console.log('starting application...'); - - const app = await NestFactory.createApplicationContext(MainModule, { - logger: new NestLogger(), - }); - const serverService = app.get(ServerService); - await serverService.launch(); - - await startControllerEndpoints(); - - // ジョブキューは必要な時にテストコード側で起動する - // ジョブキューが動くとテスト結果の確認に支障が出ることがあるので意図的に動かさないでいる - - console.log('application initialized.'); -} - -/** - * 既に重複したポートで待ち受けしているサーバがある場合はkillする - */ -async function killTestServer() { - // - try { - const pid = await portToPid(config.port); - if (pid) { - await fkill(pid, { force: true }); - } - } catch { - // NOP; - } -} - -/** - * 別プロセスに切り離してしまったが故に出来なくなった環境変数の書き換え等を実現するためのエンドポイントを作る - * @param port - */ -async function startControllerEndpoints(port = config.port + 1000) { - const fastify = Fastify(); - - fastify.post<{ Body: { key?: string, value?: string } }>('/env', async (req, res) => { - console.log(req.body); - const key = req.body['key']; - if (!key) { - res.code(400).send({ success: false }); - return; - } - - process.env[key] = req.body['value']; - - res.code(200).send({ success: true }); - }); - - fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => { - process.env = JSON.parse(originEnv); - res.code(200).send({ success: true }); - }); - - await fastify.listen({ port: port, host: 'localhost' }); -} - -export default launch; diff --git a/packages/backend/test-server/eslint.config.js b/packages/backend/test-server/eslint.config.js deleted file mode 100644 index b9c16d469f..0000000000 --- a/packages/backend/test-server/eslint.config.js +++ /dev/null @@ -1,43 +0,0 @@ -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - 'import/order': ['warn', { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - 'object', - 'type', - ], - pathGroups: [{ - pattern: '@/**', - group: 'external', - position: 'after', - }], - }], - 'no-restricted-globals': ['error', { - name: '__dirname', - message: 'Not in ESModule. Use `import.meta.url` instead.', - }, { - name: '__filename', - message: 'Not in ESModule. Use `import.meta.url` instead.', - }], - }, - }, -]; diff --git a/packages/backend/test-server/tsconfig.json b/packages/backend/test-server/tsconfig.json deleted file mode 100644 index 10313699c2..0000000000 --- a/packages/backend/test-server/tsconfig.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": false, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": true, - "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", - "allowSyntheticDefaultImports": true, - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": true, - "strictPropertyInitialization": false, - "skipLibCheck": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "isolatedModules": true, - "rootDir": "../src", - "baseUrl": "./", - "paths": { - "@/*": ["../src/*"] - }, - "outDir": "../built-test", - "types": [ - "node" - ], - "typeRoots": [ - "../src/@types", - "../node_modules/@types", - "../node_modules" - ], - "lib": [ - "esnext" - ] - }, - "compileOnSave": false, - "include": [ - "./**/*.ts", - "../src/**/*.ts" - ], - "exclude": [ - "../src/**/*.test.ts" - ] -} diff --git a/packages/backend/test/.eslintrc.cjs b/packages/backend/test/.eslintrc.cjs new file mode 100644 index 0000000000..41ecea0c3f --- /dev/null +++ b/packages/backend/test/.eslintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + extends: ['../.eslintrc.cjs'], + env: { + node: true, + jest: true, + }, +}; diff --git a/packages/backend/test/compose.yml b/packages/backend/test/docker-compose.yml similarity index 94% rename from packages/backend/test/compose.yml rename to packages/backend/test/docker-compose.yml index 408e113fd8..f51f88ccde 100644 --- a/packages/backend/test/compose.yml +++ b/packages/backend/test/docker-compose.yml @@ -1,3 +1,5 @@ +version: "3" + services: redistest: image: redis:7 diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 1d04396406..eee6757495 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,7 @@ import * as crypto from 'node:crypto'; import cbor from 'cbor'; import * as OTPAuth from 'otpauth'; import { loadConfig } from '@/config.js'; -import { api, signup } from '../utils.js'; +import { api, signup, startServer } from '../utils.js'; import type { AuthenticationResponseJSON, AuthenticatorAssertionResponseJSON, @@ -18,11 +18,13 @@ import type { PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, -} from '@simplewebauthn/types'; +} from '@simplewebauthn/typescript-types'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('2要素認証', () => { - let alice: misskey.entities.SignupResponse; + let app: INestApplicationContext; + let alice: misskey.entities.MeSignup; const config = loadConfig(); const password = 'test'; @@ -183,11 +185,16 @@ describe('2要素認証', () => { }; beforeAll(async () => { + app = await startServer(); alice = await signup({ username, password }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('が設定でき、OTPでログインできる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); @@ -197,18 +204,18 @@ describe('2要素認証', () => { assert.strictEqual(registerResponse.body.label, username); assert.strictEqual(registerResponse.body.issuer, config.host); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 200); - const usersShowResponse = await api('users/show', { + const usersShowResponse = await api('/users/show', { username, }, alice); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); + assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); - const signinResponse = await api('signin', { + const signinResponse = await api('/signin', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); @@ -216,24 +223,24 @@ describe('2要素認証', () => { assert.notEqual(signinResponse.body.i, undefined); // 後片付け - await api('i/2fa/unregister', { + await api('/i/2fa/unregister', { password, token: otpToken(registerResponse.body.secret), }, alice); }); test('が設定でき、セキュリティキーでログインできる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 200); - const registerKeyResponse = await api('i/2fa/register-key', { + const registerKeyResponse = await api('/i/2fa/register-key', { password, token: otpToken(registerResponse.body.secret), }, alice); @@ -243,58 +250,58 @@ describe('2要素認証', () => { const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); - const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({ + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ token: otpToken(registerResponse.body.secret), keyName, credentialId, creationOptions: registerKeyResponse.body, - } as any) as any, alice); + }), alice); assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.name, keyName); - const usersShowResponse = await api('users/show', { + const usersShowResponse = await api('/users/show', { username, }); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true); + assert.strictEqual(usersShowResponse.body.securityKeys, true); - const signinResponse = await api('signin', { + const signinResponse = await api('/signin', { ...signinParam(), }); assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.body.i, undefined); - assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined); - assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined); - assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url')); + assert.notEqual(signinResponse.body.challenge, undefined); + assert.notEqual(signinResponse.body.allowCredentials, undefined); + assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url')); - const signinResponse2 = await api('signin', signinWithSecurityKeyParam({ + const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({ keyName, credentialId, requestOptions: signinResponse.body, - } as any)); + })); assert.strictEqual(signinResponse2.status, 200); assert.notEqual(signinResponse2.body.i, undefined); // 後片付け - await api('i/2fa/unregister', { + await api('/i/2fa/unregister', { password, token: otpToken(registerResponse.body.secret), }, alice); }); test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 200); - const registerKeyResponse = await api('i/2fa/register-key', { + const registerKeyResponse = await api('/i/2fa/register-key', { token: otpToken(registerResponse.body.secret), password, }, alice); @@ -302,62 +309,62 @@ describe('2要素認証', () => { const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); - const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({ + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ token: otpToken(registerResponse.body.secret), keyName, credentialId, creationOptions: registerKeyResponse.body, - } as any) as any, alice); + }), alice); assert.strictEqual(keyDoneResponse.status, 200); - const passwordLessResponse = await api('i/2fa/password-less', { + const passwordLessResponse = await api('/i/2fa/password-less', { value: true, }, alice); assert.strictEqual(passwordLessResponse.status, 204); - const usersShowResponse = await api('users/show', { + const usersShowResponse = await api('/users/show', { username, }); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true); + assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true); - const signinResponse = await api('signin', { + const signinResponse = await api('/signin', { ...signinParam(), password: '', }); assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.body.i, undefined); - const signinResponse2 = await api('signin', { + const signinResponse2 = await api('/signin', { ...signinWithSecurityKeyParam({ keyName, credentialId, requestOptions: signinResponse.body, - } as any), + }), password: '', }); assert.strictEqual(signinResponse2.status, 200); assert.notEqual(signinResponse2.body.i, undefined); // 後片付け - await api('i/2fa/unregister', { + await api('/i/2fa/unregister', { password, token: otpToken(registerResponse.body.secret), }, alice); }); test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 200); - const registerKeyResponse = await api('i/2fa/register-key', { + const registerKeyResponse = await api('/i/2fa/register-key', { token: otpToken(registerResponse.body.secret), password, }, alice); @@ -365,49 +372,48 @@ describe('2要素認証', () => { const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); - const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({ + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ token: otpToken(registerResponse.body.secret), keyName, credentialId, creationOptions: registerKeyResponse.body, - } as any) as any, alice); + }), alice); assert.strictEqual(keyDoneResponse.status, 200); const renamedKey = 'other-key'; - const updateKeyResponse = await api('i/2fa/update-key', { + const updateKeyResponse = await api('/i/2fa/update-key', { name: renamedKey, credentialId: credentialId.toString('base64url'), }, alice); assert.strictEqual(updateKeyResponse.status, 200); - const iResponse = await api('i', { + const iResponse = await api('/i', { }, alice); assert.strictEqual(iResponse.status, 200); - assert.ok(iResponse.body.securityKeysList); const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url')); assert.strictEqual(securityKeys.length, 1); assert.strictEqual(securityKeys[0].name, renamedKey); assert.notEqual(securityKeys[0].lastUsed, undefined); // 後片付け - await api('i/2fa/unregister', { + await api('/i/2fa/unregister', { password, token: otpToken(registerResponse.body.secret), }, alice); }); test('が設定でき、設定したセキュリティキーを削除できる。', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 200); - const registerKeyResponse = await api('i/2fa/register-key', { + const registerKeyResponse = await api('/i/2fa/register-key', { token: otpToken(registerResponse.body.secret), password, }, alice); @@ -415,21 +421,20 @@ describe('2要素認証', () => { const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); - const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({ + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ token: otpToken(registerResponse.body.secret), keyName, credentialId, creationOptions: registerKeyResponse.body, - } as any) as any, alice); + }), alice); assert.strictEqual(keyDoneResponse.status, 200); // テストの実行順によっては複数残ってるので全部消す - const iResponse = await api('i', { + const iResponse = await api('/i', { }, alice); assert.strictEqual(iResponse.status, 200); - assert.ok(iResponse.body.securityKeysList); for (const key of iResponse.body.securityKeysList) { - const removeKeyResponse = await api('i/2fa/remove-key', { + const removeKeyResponse = await api('/i/2fa/remove-key', { token: otpToken(registerResponse.body.secret), password, credentialId: key.id, @@ -437,13 +442,13 @@ describe('2要素認証', () => { assert.strictEqual(removeKeyResponse.status, 200); } - const usersShowResponse = await api('users/show', { + const usersShowResponse = await api('/users/show', { username, }); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false); + assert.strictEqual(usersShowResponse.body.securityKeys, false); - const signinResponse = await api('signin', { + const signinResponse = await api('/signin', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); @@ -451,43 +456,43 @@ describe('2要素認証', () => { assert.notEqual(signinResponse.body.i, undefined); // 後片付け - await api('i/2fa/unregister', { + await api('/i/2fa/unregister', { password, token: otpToken(registerResponse.body.secret), }, alice); }); test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => { - const registerResponse = await api('i/2fa/register', { + const registerResponse = await api('/i/2fa/register', { password, }, alice); assert.strictEqual(registerResponse.status, 200); - const doneResponse = await api('i/2fa/done', { + const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 200); - const usersShowResponse = await api('users/show', { + const usersShowResponse = await api('/users/show', { username, }); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); + assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); - const unregisterResponse = await api('i/2fa/unregister', { + const unregisterResponse = await api('/i/2fa/unregister', { token: otpToken(registerResponse.body.secret), password, }, alice); assert.strictEqual(unregisterResponse.status, 204); - const signinResponse = await api('signin', { + const signinResponse = await api('/signin', { ...signinParam(), }); assert.strictEqual(signinResponse.status, 200); assert.notEqual(signinResponse.body.i, undefined); // 後片付け - await api('i/2fa/unregister', { + await api('/i/2fa/unregister', { password, token: otpToken(registerResponse.body.secret), }, alice); diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 1c2606271b..c94dad7a2c 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -1,24 +1,29 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { inspect } from 'node:util'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import type { Packed } from '@/misc/json-schema.js'; import { - api, - failedApiCall, + signup, post, + userList, + page, role, - signup, + startServer, + api, successfulApiCall, - testPaginationConsistency, + failedApiCall, uploadFile, - userList, + testPaginationConsistency, } from '../utils.js'; import type * as misskey from 'cherrypick-js'; +import type { INestApplicationContext } from '@nestjs/common'; const compareBy = (selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { return selector(a).localeCompare(selector(b)); @@ -28,8 +33,11 @@ describe('アンテナ', () => { // エンティティとしてのアンテナを主眼においたテストを記述する // (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからノートを取得するエンドポイントをテストする) - type Antenna = misskey.entities.Antenna; - type User = misskey.entities.SignupResponse; + // BUG cherrypick-jsとjson-schemaが一致していない。 + // - srcのenumにgroupが残っている + // - userGroupIdが残っている, isActiveがない + type Antenna = misskey.entities.Antenna | Packed<'Antenna'>; + type User = misskey.entities.MeSignup; type Note = misskey.entities.Note; // アンテナを作成できる最小のパラメタ @@ -38,15 +46,17 @@ describe('アンテナ', () => { excludeKeywords: [['']], keywords: [['keyword']], name: 'test', + notify: false, src: 'all' as const, userGroupId: null, userListId: null, users: [''], withFile: false, withReplies: false, - excludeBots: false, }; + let app: INestApplicationContext; + let root: User; let alice: User; let bob: User; @@ -70,6 +80,10 @@ describe('アンテナ', () => { let userMutingAlice: User; let userMutedByAlice: User; + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); @@ -77,7 +91,7 @@ describe('アンテナ', () => { aliceList = await userList(alice, {}); bob = await signup({ username: 'bob' }); aliceList = await userList(alice, {}); - bobFile = (await uploadFile(bob)).body!; + bobFile = (await uploadFile(bob)).body; bobList = await userList(bob); carol = await signup({ username: 'carol' }); await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice); @@ -123,12 +137,16 @@ describe('アンテナ', () => { await api('mute/create', { userId: userMutedByAlice.id }, alice); }, 1000 * 60 * 10); + afterAll(async () => { + await app.close(); + }); + beforeEach(async () => { // テスト間で影響し合わないように毎回全部消す。 for (const user of [alice, bob]) { - const list = await api('antennas/list', {}, user); + const list = await api('/antennas/list', {}, user); for (const antenna of list.body) { - await api('antennas/delete', { antennaId: antenna.id }, user); + await api('/antennas/delete', { antennaId: antenna.id }, user); } } }); @@ -138,11 +156,11 @@ describe('アンテナ', () => { test('が作成できること、キーが過不足なく入っていること。', async () => { const response = await successfulApiCall({ endpoint: 'antennas/create', - parameters: defaultParam, + parameters: { ...defaultParam }, user: alice, }); assert.match(response.id, /[0-9a-z]{10}/); - const expected: Antenna = { + const expected = { id: response.id, caseSensitive: false, createdAt: new Date(response.createdAt).toISOString(), @@ -151,21 +169,21 @@ describe('アンテナ', () => { isActive: true, keywords: [['keyword']], name: 'test', + notify: false, src: 'all', userGroupId: null, userListId: null, users: [''], withFile: false, withReplies: false, - excludeBots: false, localOnly: false, - notify: false, - }; + } as Antenna; assert.deepStrictEqual(response, expected); }); test('が上限いっぱいまで作成できること', async () => { - const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit)].map(() => successfulApiCall({ + // antennaLimit + 1まで作れるのがキモ + const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({ endpoint: 'antennas/create', parameters: { ...defaultParam }, user: alice, @@ -200,26 +218,28 @@ describe('アンテナ', () => { }); const antennaParamPattern = [ - { parameters: () => ({ name: 'x'.repeat(100) }) }, - { parameters: () => ({ name: 'x' }) }, - { parameters: () => ({ src: 'home' as const }) }, - { parameters: () => ({ src: 'all' as const }) }, - { parameters: () => ({ src: 'users' as const }) }, - { parameters: () => ({ src: 'list' as const }) }, - { parameters: () => ({ userGroupId: null }) }, - { parameters: () => ({ userListId: null }) }, - { parameters: () => ({ src: 'list' as const, userListId: aliceList.id }) }, - { parameters: () => ({ keywords: [['x']] }) }, - { parameters: () => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, - { parameters: () => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, - { parameters: () => ({ users: [alice.username] }) }, - { parameters: () => ({ users: [alice.username, bob.username, carol.username] }) }, - { parameters: () => ({ caseSensitive: false }) }, - { parameters: () => ({ caseSensitive: true }) }, - { parameters: () => ({ withReplies: false }) }, - { parameters: () => ({ withReplies: true }) }, - { parameters: () => ({ withFile: false }) }, - { parameters: () => ({ withFile: true }) }, + { parameters: (): object => ({ name: 'x'.repeat(100) }) }, + { parameters: (): object => ({ name: 'x' }) }, + { parameters: (): object => ({ src: 'home' }) }, + { parameters: (): object => ({ src: 'all' }) }, + { parameters: (): object => ({ src: 'users' }) }, + { parameters: (): object => ({ src: 'list' }) }, + { parameters: (): object => ({ userGroupId: null }) }, + { parameters: (): object => ({ userListId: null }) }, + { parameters: (): object => ({ src: 'list', userListId: aliceList.id }) }, + { parameters: (): object => ({ keywords: [['x']] }) }, + { parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, + { parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, + { parameters: (): object => ({ users: [alice.username] }) }, + { parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) }, + { parameters: (): object => ({ caseSensitive: false }) }, + { parameters: (): object => ({ caseSensitive: true }) }, + { parameters: (): object => ({ withReplies: false }) }, + { parameters: (): object => ({ withReplies: true }) }, + { parameters: (): object => ({ withFile: false }) }, + { parameters: (): object => ({ withFile: true }) }, + { parameters: (): object => ({ notify: false }) }, + { parameters: (): object => ({ notify: true }) }, ]; test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => { const response = await successfulApiCall({ @@ -332,7 +352,7 @@ describe('アンテナ', () => { test.each([ { label: '全体から', - parameters: () => ({ src: 'all' }), + parameters: (): object => ({ src: 'all' }), posts: [ { note: (): Promise => post(alice, { text: `${keyword}` }), included: true }, { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, @@ -343,7 +363,7 @@ describe('アンテナ', () => { { // BUG e4144a1 以降home指定は壊れている(allと同じ) label: 'ホーム指定はallと同じ', - parameters: () => ({ src: 'home' }), + parameters: (): object => ({ src: 'home' }), posts: [ { note: (): Promise => post(alice, { text: `${keyword}` }), included: true }, { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, @@ -354,7 +374,7 @@ describe('アンテナ', () => { { // https://github.com/misskey-dev/misskey/issues/9025 label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true }, { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true }, @@ -364,56 +384,56 @@ describe('アンテナ', () => { }, { label: 'ブロックしているユーザーのノートは含む', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userBlockedByAlice, { text: `${keyword}` }), included: true }, ], }, { label: 'ブロックされているユーザーのノートは含まない', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userBlockingAlice, { text: `${keyword}` }) }, ], }, { label: 'ミュートしているユーザーのノートは含まない', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userMutedByAlice, { text: `${keyword}` }) }, ], }, { label: 'ミュートされているユーザーのノートは含む', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userMutingAlice, { text: `${keyword}` }), included: true }, ], }, { label: '「見つけやすくする」がOFFのユーザーのノートも含まれる', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userNotExplorable, { text: `${keyword}` }), included: true }, ], }, { label: '鍵付きユーザーのノートも含まれる', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userLocking, { text: `${keyword}` }), included: true }, ], }, { label: 'サイレンスのノートも含まれる', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userSilenced, { text: `${keyword}` }), included: true }, ], }, { label: '削除ユーザーのノートも含まれる', - parameters: () => ({}), + parameters: (): object => ({}), posts: [ { note: (): Promise => post(userDeletedBySelf, { text: `${keyword}` }), included: true }, { note: (): Promise => post(userDeletedByAdmin, { text: `${keyword}` }), included: true }, @@ -421,7 +441,7 @@ describe('アンテナ', () => { }, { label: 'ユーザー指定で', - parameters: () => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }), + parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }), posts: [ { note: (): Promise => post(alice, { text: `test ${keyword}` }) }, { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, @@ -430,7 +450,7 @@ describe('アンテナ', () => { }, { label: 'リスト指定で', - parameters: () => ({ src: 'list', userListId: aliceList.id }), + parameters: (): object => ({ src: 'list', userListId: aliceList.id }), posts: [ { note: (): Promise => post(alice, { text: `test ${keyword}` }) }, { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, @@ -439,14 +459,14 @@ describe('アンテナ', () => { }, { label: 'CWにもマッチする', - parameters: () => ({ keywords: [[keyword]] }), + parameters: (): object => ({ keywords: [[keyword]] }), posts: [ { note: (): Promise => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true }, ], }, { label: 'キーワード1つ', - parameters: () => ({ keywords: [[keyword]] }), + parameters: (): object => ({ keywords: [[keyword]] }), posts: [ { note: (): Promise => post(alice, { text: 'test' }) }, { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, @@ -455,7 +475,7 @@ describe('アンテナ', () => { }, { label: 'キーワード3つ(AND)', - parameters: () => ({ keywords: [['A', 'B', 'C']] }), + parameters: (): object => ({ keywords: [['A', 'B', 'C']] }), posts: [ { note: (): Promise => post(bob, { text: 'test A' }) }, { note: (): Promise => post(bob, { text: 'test A B' }) }, @@ -466,7 +486,7 @@ describe('アンテナ', () => { }, { label: 'キーワード3つ(OR)', - parameters: () => ({ keywords: [['A'], ['B'], ['C']] }), + parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }), posts: [ { note: (): Promise => post(bob, { text: 'test' }) }, { note: (): Promise => post(bob, { text: 'test A' }), included: true }, @@ -479,7 +499,7 @@ describe('アンテナ', () => { }, { label: '除外ワード3つ(AND)', - parameters: () => ({ excludeKeywords: [['A', 'B', 'C']] }), + parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }), posts: [ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise => post(bob, { text: `test ${keyword} A` }), included: true }, @@ -492,7 +512,7 @@ describe('アンテナ', () => { }, { label: '除外ワード3つ(OR)', - parameters: () => ({ excludeKeywords: [['A'], ['B'], ['C']] }), + parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }), posts: [ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise => post(bob, { text: `test ${keyword} A` }) }, @@ -505,7 +525,7 @@ describe('アンテナ', () => { }, { label: 'キーワード1つ(大文字小文字区別する)', - parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: true }), + parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }), posts: [ { note: (): Promise => post(bob, { text: 'keyword' }) }, { note: (): Promise => post(bob, { text: 'kEyWoRd' }) }, @@ -514,7 +534,7 @@ describe('アンテナ', () => { }, { label: 'キーワード1つ(大文字小文字区別しない)', - parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: false }), + parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }), posts: [ { note: (): Promise => post(bob, { text: 'keyword' }), included: true }, { note: (): Promise => post(bob, { text: 'kEyWoRd' }), included: true }, @@ -523,7 +543,7 @@ describe('アンテナ', () => { }, { label: '除外ワード1つ(大文字小文字区別する)', - parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }), + parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise => post(bob, { text: `${keyword} keyword` }), included: true }, @@ -533,7 +553,7 @@ describe('アンテナ', () => { }, { label: '除外ワード1つ(大文字小文字区別しない)', - parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }), + parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise => post(bob, { text: `${keyword} keyword` }) }, @@ -543,7 +563,7 @@ describe('アンテナ', () => { }, { label: '添付ファイルを問わない', - parameters: () => ({ withFile: false }), + parameters: (): object => ({ withFile: false }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, @@ -551,7 +571,7 @@ describe('アンテナ', () => { }, { label: '添付ファイル付きのみ', - parameters: () => ({ withFile: true }), + parameters: (): object => ({ withFile: true }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, { note: (): Promise => post(bob, { text: `${keyword}` }) }, @@ -559,7 +579,7 @@ describe('アンテナ', () => { }, { label: 'リプライ以外', - parameters: () => ({ withReplies: false }), + parameters: (): object => ({ withReplies: false }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}`, replyId: alicePost.id }) }, { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, @@ -567,7 +587,7 @@ describe('アンテナ', () => { }, { label: 'リプライも含む', - parameters: () => ({ withReplies: true }), + parameters: (): object => ({ withReplies: true }), posts: [ { note: (): Promise => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true }, { note: (): Promise => post(bob, { text: `${keyword}` }), included: true }, @@ -630,7 +650,7 @@ describe('アンテナ', () => { endpoint: 'antennas/notes', parameters: { antennaId: antenna.id, ...paginationParam }, user: alice, - }); + }) as any as Note[]; }, offsetBy, 'desc'); }); diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index 84c868353d..658cbfc4f9 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -1,61 +1,72 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { UserToken, api, post, signup } from '../utils.js'; +import { signup, api, post, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('API visibility', () => { + let app: INestApplicationContext; + + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + describe('Note visibility', () => { //#region vars /** ヒロイン */ - let alice: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; /** フォロワー */ - let follower: misskey.entities.SignupResponse; + let follower: misskey.entities.MeSignup; /** 非フォロワー */ - let other: misskey.entities.SignupResponse; + let other: misskey.entities.MeSignup; /** 非フォロワーでもリプライやメンションをされた人 */ - let target: misskey.entities.SignupResponse; + let target: misskey.entities.MeSignup; /** specified mentionでmentionを飛ばされる人 */ - let target2: misskey.entities.SignupResponse; + let target2: misskey.entities.MeSignup; /** public-post */ - let pub: misskey.entities.Note; + let pub: any; /** home-post */ - let home: misskey.entities.Note; + let home: any; /** followers-post */ - let fol: misskey.entities.Note; + let fol: any; /** specified-post */ - let spe: misskey.entities.Note; + let spe: any; /** public-reply to target's post */ - let pubR: misskey.entities.Note; + let pubR: any; /** home-reply to target's post */ - let homeR: misskey.entities.Note; + let homeR: any; /** followers-reply to target's post */ - let folR: misskey.entities.Note; + let folR: any; /** specified-reply to target's post */ - let speR: misskey.entities.Note; + let speR: any; /** public-mention to target */ - let pubM: misskey.entities.Note; + let pubM: any; /** home-mention to target */ - let homeM: misskey.entities.Note; + let homeM: any; /** followers-mention to target */ - let folM: misskey.entities.Note; + let folM: any; /** specified-mention to target */ - let speM: misskey.entities.Note; + let speM: any; /** reply target post */ - let tgt: misskey.entities.Note; + let tgt: any; //#endregion - const show = async (noteId: misskey.entities.Note['id'], by?: UserToken) => { - return await api('notes/show', { + const show = async (noteId: any, by: any) => { + return await api('/notes/show', { noteId, }, by); }; @@ -70,7 +81,7 @@ describe('API visibility', () => { target2 = await signup({ username: 'target2' }); // follow alice <= follower - await api('following/create', { userId: alice.id }, follower); + await api('/following/create', { userId: alice.id }, follower); // normal posts pub = await post(alice, { text: 'x', visibility: 'public' }); @@ -111,7 +122,7 @@ describe('API visibility', () => { }); test('[show] public-postを未認証が見れる', async () => { - const res = await show(pub.id); + const res = await show(pub.id, null); assert.strictEqual(res.body.text, 'x'); }); @@ -132,7 +143,7 @@ describe('API visibility', () => { }); test('[show] home-postを未認証が見れる', async () => { - const res = await show(home.id); + const res = await show(home.id, null); assert.strictEqual(res.body.text, 'x'); }); @@ -153,7 +164,7 @@ describe('API visibility', () => { }); test('[show] followers-postを未認証が見れない', async () => { - const res = await show(fol.id); + const res = await show(fol.id, null); assert.strictEqual(res.body.isHidden, true); }); @@ -179,7 +190,7 @@ describe('API visibility', () => { }); test('[show] specified-postを未認証が見れない', async () => { - const res = await show(spe.id); + const res = await show(spe.id, null); assert.strictEqual(res.body.isHidden, true); }); //#endregion @@ -207,7 +218,7 @@ describe('API visibility', () => { }); test('[show] public-replyを未認証が見れる', async () => { - const res = await show(pubR.id); + const res = await show(pubR.id, null); assert.strictEqual(res.body.text, 'x'); }); @@ -233,7 +244,7 @@ describe('API visibility', () => { }); test('[show] home-replyを未認証が見れる', async () => { - const res = await show(homeR.id); + const res = await show(homeR.id, null); assert.strictEqual(res.body.text, 'x'); }); @@ -259,7 +270,7 @@ describe('API visibility', () => { }); test('[show] followers-replyを未認証が見れない', async () => { - const res = await show(folR.id); + const res = await show(folR.id, null); assert.strictEqual(res.body.isHidden, true); }); @@ -290,7 +301,7 @@ describe('API visibility', () => { }); test('[show] specified-replyを未認証が見れない', async () => { - const res = await show(speR.id); + const res = await show(speR.id, null); assert.strictEqual(res.body.isHidden, true); }); //#endregion @@ -318,7 +329,7 @@ describe('API visibility', () => { }); test('[show] public-mentionを未認証が見れる', async () => { - const res = await show(pubM.id); + const res = await show(pubM.id, null); assert.strictEqual(res.body.text, '@target x'); }); @@ -344,7 +355,7 @@ describe('API visibility', () => { }); test('[show] home-mentionを未認証が見れる', async () => { - const res = await show(homeM.id); + const res = await show(homeM.id, null); assert.strictEqual(res.body.text, '@target x'); }); @@ -370,7 +381,7 @@ describe('API visibility', () => { }); test('[show] followers-mentionを未認証が見れない', async () => { - const res = await show(folM.id); + const res = await show(folM.id, null); assert.strictEqual(res.body.isHidden, true); }); @@ -401,69 +412,69 @@ describe('API visibility', () => { }); test('[show] specified-mentionを未認証が見れない', async () => { - const res = await show(speM.id); + const res = await show(speM.id, null); assert.strictEqual(res.body.isHidden, true); }); //#endregion //#region HTL test('[HTL] public-post が 自分が見れる', async () => { - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === pub.id); + const notes = res.body.filter((n: any) => n.id === pub.id); assert.strictEqual(notes[0].text, 'x'); }); test('[HTL] public-post が 非フォロワーから見れない', async () => { - const res = await api('notes/timeline', { limit: 100 }, other); + const res = await api('/notes/timeline', { limit: 100 }, other); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === pub.id); + const notes = res.body.filter((n: any) => n.id === pub.id); assert.strictEqual(notes.length, 0); }); test('[HTL] followers-post が フォロワーから見れる', async () => { - const res = await api('notes/timeline', { limit: 100 }, follower); + const res = await api('/notes/timeline', { limit: 100 }, follower); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === fol.id); + const notes = res.body.filter((n: any) => n.id === fol.id); assert.strictEqual(notes[0].text, 'x'); }); //#endregion //#region RTL test('[replies] followers-reply が フォロワーから見れる', async () => { - const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folR.id); + const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { - const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folR.id); + const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes.length, 0); }); test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { - const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folR.id); + const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); //#endregion //#region MTL test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { - const res = await api('notes/mentions', { limit: 100 }, target); + const res = await api('/notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folR.id); + const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { - const res = await api('notes/mentions', { limit: 100 }, target); + const res = await api('/notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter(n => n.id === folM.id); + const notes = res.body.filter((n: any) => n.id === folM.id); assert.strictEqual(notes[0].text, '@target x'); }); //#endregion diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index bc6e46421a..3507d46001 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,48 +7,45 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { IncomingMessage } from 'http'; -import { - api, - connectStream, - createAppToken, - failedApiCall, - relativeFetch, - signup, - successfulApiCall, - uploadFile, - waitFire, -} from '../utils.js'; +import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('API', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; + let app: INestApplicationContext; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + describe('General validation', () => { test('wrong type', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, - // @ts-expect-error string must be string string: 42, }); assert.strictEqual(res.status, 400); }); test('missing require param', async () => { - // @ts-expect-error required is required - const res = await api('test', { + const res = await api('/test', { string: 'a', }); assert.strictEqual(res.status, 400); }); test('invalid misskey:id (empty string)', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, id: '', }); @@ -56,7 +53,7 @@ describe('API', () => { }); test('valid misskey:id', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, id: '8wvhjghbxu', }); @@ -64,7 +61,7 @@ describe('API', () => { }); test('default value', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, string: 'a', }); @@ -73,7 +70,7 @@ describe('API', () => { }); test('can set null even if it has default value', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, nullableDefault: null, }); @@ -82,7 +79,7 @@ describe('API', () => { }); test('cannot set undefined if it has default value', async () => { - const res = await api('test', { + const res = await api('/test', { required: true, nullableDefault: undefined, }); @@ -99,14 +96,14 @@ describe('API', () => { // aliceは管理者、APIを使える await successfulApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: alice, }); // bobは一般ユーザーだからダメ await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: bob, }, { @@ -117,7 +114,7 @@ describe('API', () => { // publicアクセスももちろんダメ await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: undefined, }, { @@ -128,7 +125,7 @@ describe('API', () => { // ごまがしもダメ await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: { token: 'tsukawasete' }, }, { @@ -138,13 +135,13 @@ describe('API', () => { }); await successfulApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: { token: application2 }, }); await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: { token: application }, }, { @@ -154,7 +151,7 @@ describe('API', () => { }); await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: { token: application3 }, }, { @@ -164,7 +161,7 @@ describe('API', () => { }); await failedApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: { token: application4 }, }, { @@ -177,7 +174,7 @@ describe('API', () => { describe('Authentication header', () => { test('一般リクエスト', async () => { await successfulApiCall({ - endpoint: 'admin/get-index-stats', + endpoint: '/admin/get-index-stats', parameters: {}, user: { token: alice.token, @@ -211,7 +208,7 @@ describe('API', () => { describe('tokenエラー応答でWWW-Authenticate headerを送る', () => { describe('invalid_token', () => { test('一般リクエスト', async () => { - const result = await api('admin/get-index-stats', {}, { + const result = await api('/admin/get-index-stats', {}, { token: 'noridev', bearer: true, }); @@ -246,7 +243,7 @@ describe('API', () => { describe('tokenがないとrealmだけおくる', () => { test('一般リクエスト', async () => { - const result = await api('admin/get-index-stats', {}); + const result = await api('/admin/get-index-stats', {}); assert.strictEqual(result.status, 401); assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="CherryPick"'); }); @@ -259,8 +256,7 @@ describe('API', () => { }); test('invalid_request', async () => { - // @ts-expect-error text must be string - const result = await api('notes/create', { text: true }, { + const result = await api('/notes/create', { text: true }, { token: alice.token, bearer: true, }); diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index 08a9ea5137..3505b5572a 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -1,28 +1,36 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, castAsError, post, signup } from '../utils.js'; +import { signup, api, post, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('Block', () => { + let app: INestApplicationContext; + // alice blocks bob - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('Block作成', async () => { - const res = await api('blocking/create', { + const res = await api('/blocking/create', { userId: bob.id, }, alice); @@ -30,39 +38,37 @@ describe('Block', () => { }); test('ブロックされているユーザーをフォローできない', async () => { - const res = await api('following/create', { userId: alice.id }, bob); + const res = await api('/following/create', { userId: alice.id }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); + assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); }); test('ブロックされているユーザーにリアクションできない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); + const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); assert.strictEqual(res.status, 400); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); + assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); }); test('ブロックされているユーザーに返信できない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob); + const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); }); test('ブロックされているユーザーのノートをRenoteできない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob); + const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); }); // TODO: ユーザーリストに入れられないテスト @@ -74,13 +80,12 @@ describe('Block', () => { const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); - const res = await api('notes/local-timeline', {}, bob); - const body = res.body as misskey.entities.Note[]; + const res = await api('/notes/local-timeline', {}, bob); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(body.some(note => note.id === aliceNote.id), false); - assert.strictEqual(body.some(note => note.id === bobNote.id), true); - assert.strictEqual(body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); }); diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index d38deb18b0..25ec521d2c 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -1,39 +1,64 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { JTDDataType } from 'ajv/dist/jtd'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js'; -import type * as Misskey from 'cherrypick-js'; - -type Optional = Pick, K> & Omit; +import type { Packed } from '@/misc/json-schema.js'; +import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js'; +import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js'; +import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js'; +import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js'; +import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js'; +import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js'; +import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js'; +import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js'; +import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js'; +import { + signup, + post, + startServer, + api, + successfulApiCall, + failedApiCall, + ApiRequest, + hiddenNote, +} from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('クリップ', () => { - let alice: Misskey.entities.SignupResponse; - let bob: Misskey.entities.SignupResponse; - let aliceNote: Misskey.entities.Note; - let aliceHomeNote: Misskey.entities.Note; - let aliceFollowersNote: Misskey.entities.Note; - let aliceSpecifiedNote: Misskey.entities.Note; - let bobNote: Misskey.entities.Note; - let bobHomeNote: Misskey.entities.Note; - let bobFollowersNote: Misskey.entities.Note; - let bobSpecifiedNote: Misskey.entities.Note; + type User = Packed<'User'>; + type Note = Packed<'Note'>; + type Clip = Packed<'Clip'>; + + let app: INestApplicationContext; + + let alice: User; + let bob: User; + let aliceNote: Note; + let aliceHomeNote: Note; + let aliceFollowersNote: Note; + let aliceSpecifiedNote: Note; + let bobNote: Note; + let bobHomeNote: Note; + let bobFollowersNote: Note; + let bobSpecifiedNote: Note; const compareBy = (selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { return selector(a).localeCompare(selector(b)); }; - const defaultCreate = (): Pick => ({ + type CreateParam = JTDDataType; + const defaultCreate = (): Partial => ({ name: 'test', }); - const create = async (parameters: Partial = {}, request: Partial> = {}): Promise => { - const clip = await successfulApiCall({ - endpoint: 'clips/create', + const create = async (parameters: Partial = {}, request: Partial = {}): Promise => { + const clip = await successfulApiCall({ + endpoint: '/clips/create', parameters: { ...defaultCreate(), ...parameters, @@ -51,16 +76,17 @@ describe('クリップ', () => { return clip; }; - const createMany = async (parameters: Partial, count = 10, user = alice): Promise => { + const createMany = async (parameters: Partial, count = 10, user = alice): Promise => { return await Promise.all([...Array(count)].map((_, i) => create({ name: `test${i}`, ...parameters, }, { user }))); }; - const update = async (parameters: Optional, request: Partial> = {}): Promise => { - const clip = await successfulApiCall({ - endpoint: 'clips/update', + type UpdateParam = JTDDataType; + const update = async (parameters: Partial, request: Partial = {}): Promise => { + const clip = await successfulApiCall({ + endpoint: '/clips/update', parameters: { name: 'updated', ...parameters, @@ -78,9 +104,10 @@ describe('クリップ', () => { return clip; }; - const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial> = {}): Promise => { - await successfulApiCall({ - endpoint: 'clips/delete', + type DeleteParam = JTDDataType; + const deleteClip = async (parameters: DeleteParam, request: Partial = {}): Promise => { + return await successfulApiCall({ + endpoint: '/clips/delete', parameters, user: alice, ...request, @@ -89,53 +116,60 @@ describe('クリップ', () => { }); }; - const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial> = {}): Promise => { - return await successfulApiCall({ - endpoint: 'clips/show', + type ShowParam = JTDDataType; + const show = async (parameters: ShowParam, request: Partial = {}): Promise => { + return await successfulApiCall({ + endpoint: '/clips/show', parameters, user: alice, ...request, }); }; - const list = async (request: Partial>): Promise => { - return successfulApiCall({ - endpoint: 'clips/list', + const list = async (request: Partial): Promise => { + return successfulApiCall({ + endpoint: '/clips/list', parameters: {}, user: alice, ...request, }); }; - const usersClips = async (parameters: Misskey.entities.UsersClipsRequest, request: Partial> = {}): Promise => { - return await successfulApiCall({ - endpoint: 'users/clips', - parameters, + const usersClips = async (request: Partial): Promise => { + return await successfulApiCall({ + endpoint: '/users/clips', + parameters: {}, user: alice, ...request, }); }; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - aliceNote = await post(alice, { text: 'test' }); - aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }); - aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }); - aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }); - bobNote = await post(bob, { text: 'test' }); - bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }); - bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }); - bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }); + // FIXME: cherrypick-jsのNoteはoutdatedなので直接変換できない + aliceNote = await post(alice, { text: 'test' }) as any; + aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; + aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; + aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; + bobNote = await post(bob, { text: 'test' }) as any; + bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; + bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; + bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + afterEach(async () => { // テスト間で影響し合わないように毎回全部消す。 for (const user of [alice, bob]) { - const list = await api('clips/list', { limit: 11 }, user); + const list = await api('/clips/list', { limit: 11 }, user); for (const clip of list.body) { - await api('clips/delete', { clipId: clip.id }, user); + await api('/clips/delete', { clipId: clip.id }, user); } } }); @@ -153,13 +187,14 @@ describe('クリップ', () => { }); test('の作成はポリシーで定められた数以上はできない。', async () => { - const clipLimit = DEFAULT_POLICIES.clipLimit; + // ポリシー + 1まで作れるという所がミソ + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; for (let i = 0; i < clipLimit; i++) { await create(); } await failedApiCall({ - endpoint: 'clips/create', + endpoint: '/clips/create', parameters: defaultCreate(), user: alice, }, { @@ -186,8 +221,7 @@ describe('クリップ', () => { { label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } }, ]; test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({ - endpoint: 'clips/create', - // @ts-expect-error invalid params + endpoint: '/clips/create', parameters: { ...defaultCreate(), ...parameters, @@ -229,15 +263,15 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', } }, - { label: '他人のクリップ', user: () => bob, assertion: { + { label: '他人のクリップ', user: (): User => bob, assertion: { code: 'NO_SUCH_CLIP', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', } }, ...createClipDenyPattern as any, ])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/update', + endpoint: '/clips/update', parameters: { - clipId: (await create({}, { user: (user ?? (() => alice))() })).id, + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, name: 'updated', ...parameters, }, @@ -262,15 +296,14 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: '70ca08ba-6865-4630-b6fb-8494759aa754', } }, - { label: '他人のクリップ', user: () => bob, assertion: { + { label: '他人のクリップ', user: (): User => bob, assertion: { code: 'NO_SUCH_CLIP', id: '70ca08ba-6865-4630-b6fb-8494759aa754', } }, ])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/delete', + endpoint: '/clips/delete', parameters: { - // @ts-expect-error clipId must not be null - clipId: (await create({}, { user: (user ?? (() => alice))() })).id, + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, user: alice, @@ -290,7 +323,7 @@ describe('クリップ', () => { test('のID指定取得は他人のPrivateなクリップは取得できない', async () => { const clip = await create({ isPublic: false }, { user: bob } ); failedApiCall({ - endpoint: 'clips/show', + endpoint: '/clips/show', parameters: { clipId: clip.id }, user: alice, }, { @@ -307,8 +340,7 @@ describe('クリップ', () => { id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', } }, ])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({ - endpoint: 'clips/show', - // @ts-expect-error clipId must not be undefined + endpoint: '/clips/show', parameters: { ...parameters, }, @@ -326,7 +358,7 @@ describe('クリップ', () => { }); test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => { - const clipLimit = DEFAULT_POLICIES.clipLimit; + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; const clips = await createMany({}, clipLimit); const res = await list({ parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる @@ -341,23 +373,27 @@ describe('クリップ', () => { test('の一覧が取得できる(空)', async () => { const res = await usersClips({ - userId: alice.id, + parameters: { + userId: alice.id, + }, }); assert.deepStrictEqual(res, []); }); test.each([ { label: '' }, - { label: '他人アカウントから', user: () => bob }, + { label: '他人アカウントから', user: (): User => bob }, ])('の一覧が$label取得できる', async () => { const clips = await createMany({ isPublic: true }); const res = await usersClips({ - userId: alice.id, + parameters: { + userId: alice.id, + }, }); // 返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), clips.sort(compareBy(s => s.id))); // 認証状態で見たときだけisFavoritedが入っている @@ -367,16 +403,17 @@ describe('クリップ', () => { }); test.each([ - { label: '未認証', user: () => undefined }, + { label: '未認証', user: (): undefined => undefined }, { label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } }, ])('の一覧は$labelでも取得できる', async ({ parameters, user }) => { const clips = await createMany({ isPublic: true }); const res = await usersClips({ - userId: alice.id, - limit: clips.length, - ...parameters, - }, { - user: (user ?? (() => alice))(), + parameters: { + userId: alice.id, + limit: clips.length, + ...parameters, + }, + user: (user ?? ((): User => alice))(), }); // 未認証で見たときはisFavoritedは入らない @@ -389,8 +426,10 @@ describe('クリップ', () => { await create({ isPublic: false }); const aliceClip = await create({ isPublic: true }); const res = await usersClips({ - userId: alice.id, - limit: 2, + parameters: { + userId: alice.id, + limit: 2, + }, }); assert.deepStrictEqual(res, [aliceClip]); }); @@ -399,15 +438,17 @@ describe('クリップ', () => { const clips = await createMany({ isPublic: true }, 7); clips.sort(compareBy(s => s.id)); const res = await usersClips({ - userId: alice.id, - sinceId: clips[1].id, - untilId: clips[5].id, - limit: 4, + parameters: { + userId: alice.id, + sinceId: clips[1].id, + untilId: clips[5].id, + limit: 4, + }, }); // Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), [clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id)); }); @@ -417,9 +458,8 @@ describe('クリップ', () => { { label: 'limitゼロ', parameters: { limit: 0 } }, { label: 'limit最大+1', parameters: { limit: 101 } }, ])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({ - endpoint: 'users/clips', + endpoint: '/users/clips', parameters: { - // @ts-expect-error userId must not be undefined userId: alice.id, ...parameters, }, @@ -431,15 +471,15 @@ describe('クリップ', () => { })); test.each([ - { label: '作成', endpoint: 'clips/create' as const }, - { label: '更新', endpoint: 'clips/update' as const }, - { label: '削除', endpoint: 'clips/delete' as const }, - { label: '取得', endpoint: 'clips/list' as const }, - { label: 'お気に入り設定', endpoint: 'clips/favorite' as const }, - { label: 'お気に入り解除', endpoint: 'clips/unfavorite' as const }, - { label: 'お気に入り取得', endpoint: 'clips/my-favorites' as const }, - { label: 'ノート追加', endpoint: 'clips/add-note' as const }, - { label: 'ノート削除', endpoint: 'clips/remove-note' as const }, + { label: '作成', endpoint: '/clips/create' }, + { label: '更新', endpoint: '/clips/update' }, + { label: '削除', endpoint: '/clips/delete' }, + { label: '取得', endpoint: '/clips/list' }, + { label: 'お気に入り設定', endpoint: '/clips/favorite' }, + { label: 'お気に入り解除', endpoint: '/clips/unfavorite' }, + { label: 'お気に入り取得', endpoint: '/clips/my-favorites' }, + { label: 'ノート追加', endpoint: '/clips/add-note' }, + { label: 'ノート削除', endpoint: '/clips/remove-note' }, ])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({ endpoint: endpoint, parameters: {}, @@ -451,11 +491,12 @@ describe('クリップ', () => { })); describe('のお気に入り', () => { - let aliceClip: Misskey.entities.Clip; + let aliceClip: Clip; - const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial> = {}): Promise => { - await successfulApiCall({ - endpoint: 'clips/favorite', + type FavoriteParam = JTDDataType; + const favorite = async (parameters: FavoriteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/favorite', parameters, user: alice, ...request, @@ -464,9 +505,10 @@ describe('クリップ', () => { }); }; - const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial> = {}): Promise => { - await successfulApiCall({ - endpoint: 'clips/unfavorite', + type UnfavoriteParam = JTDDataType; + const unfavorite = async (parameters: UnfavoriteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/unfavorite', parameters, user: alice, ...request, @@ -475,9 +517,9 @@ describe('クリップ', () => { }); }; - const myFavorites = async (request: Partial> = {}): Promise => { - return successfulApiCall({ - endpoint: 'clips/my-favorites', + const myFavorites = async (request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/my-favorites', parameters: {}, user: alice, ...request, @@ -543,7 +585,7 @@ describe('クリップ', () => { test('は同じクリップに対して二回設定できない。', async () => { await favorite({ clipId: aliceClip.id }); await failedApiCall({ - endpoint: 'clips/favorite', + endpoint: '/clips/favorite', parameters: { clipId: aliceClip.id, }, @@ -561,15 +603,14 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', } }, - { label: '他人のクリップ', user: () => bob, assertion: { + { label: '他人のクリップ', user: (): User => bob, assertion: { code: 'NO_SUCH_CLIP', id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', } }, ])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/favorite', + endpoint: '/clips/favorite', parameters: { - // @ts-expect-error clipId must not be null - clipId: (await create({}, { user: (user ?? (() => alice))() })).id, + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, user: alice, @@ -595,7 +636,7 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: '2603966e-b865-426c-94a7-af4a01241dc1', } }, - { label: '他人のクリップ', user: () => bob, assertion: { + { label: '他人のクリップ', user: (): User => bob, assertion: { code: 'NOT_FAVORITED', id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', } }, @@ -604,10 +645,9 @@ describe('クリップ', () => { id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', } }, ])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/unfavorite', + endpoint: '/clips/unfavorite', parameters: { - // @ts-expect-error clipId must not be null - clipId: (await create({}, { user: (user ?? (() => alice))() })).id, + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, user: alice, @@ -632,38 +672,41 @@ describe('クリップ', () => { }); describe('に紐づくノート', () => { - let aliceClip: Misskey.entities.Clip; + let aliceClip: Clip; - const sampleNotes = (): Misskey.entities.Note[] => [ + const sampleNotes = (): Note[] => [ aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote, ]; - const addNote = async (parameters: Misskey.entities.ClipsAddNoteRequest, request: Partial> = {}): Promise => { - return successfulApiCall({ - endpoint: 'clips/add-note', + type AddNoteParam = JTDDataType; + const addNote = async (parameters: AddNoteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/add-note', parameters, user: alice, ...request, }, { status: 204, - }) as any as void; + }); }; - const removeNote = async (parameters: Misskey.entities.ClipsRemoveNoteRequest, request: Partial> = {}): Promise => { - return successfulApiCall({ - endpoint: 'clips/remove-note', + type RemoveNoteParam = JTDDataType; + const removeNote = async (parameters: RemoveNoteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/remove-note', parameters, user: alice, ...request, }, { status: 204, - }) as any as void; + }); }; - const notes = async (parameters: Misskey.entities.ClipsNotesRequest, request: Partial> = {}): Promise => { - return successfulApiCall({ - endpoint: 'clips/notes', + type NotesParam = JTDDataType; + const notes = async (parameters: Partial, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/notes', parameters, user: alice, ...request, @@ -689,7 +732,7 @@ describe('クリップ', () => { test('として同じノートを二回紐づけることはできない', async () => { await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); await failedApiCall({ - endpoint: 'clips/add-note', + endpoint: '/clips/add-note', parameters: { clipId: aliceClip.id, noteId: aliceNote.id, @@ -704,14 +747,14 @@ describe('クリップ', () => { // TODO: 17000msくらいかかる... test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => { - const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit; + const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { text: `test ${i}`, - }) as unknown)) as Misskey.entities.Note[]; + }) as unknown)) as Note[]; await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); await failedApiCall({ - endpoint: 'clips/add-note', + endpoint: '/clips/add-note', parameters: { clipId: aliceClip.id, noteId: aliceNote.id, @@ -725,7 +768,7 @@ describe('クリップ', () => { }); test('は他人のクリップへ追加できない。', async () => await failedApiCall({ - endpoint: 'clips/add-note', + endpoint: '/clips/add-note', parameters: { clipId: aliceClip.id, noteId: aliceNote.id, @@ -748,20 +791,18 @@ describe('クリップ', () => { code: 'NO_SUCH_NOTE', id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b', } }, - { label: '他人のクリップ', user: () => bob, assetion: { + { label: '他人のクリップ', user: (): object => bob, assetion: { code: 'NO_SUCH_CLIP', id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', } }, ])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ - endpoint: 'clips/add-note', + endpoint: '/clips/add-note', parameters: { - // @ts-expect-error clipId must not be undefined clipId: aliceClip.id, - // @ts-expect-error noteId must not be undefined noteId: aliceNote.id, ...parameters, }, - user: (user ?? (() => alice))(), + user: (user ?? ((): User => alice))(), }, { status: 400, code: 'INVALID_PARAM', @@ -786,20 +827,18 @@ describe('クリップ', () => { code: 'NO_SUCH_NOTE', id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる } }, - { label: '他人のクリップ', user: () => bob, assetion: { + { label: '他人のクリップ', user: (): object => bob, assetion: { code: 'NO_SUCH_CLIP', id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる } }, ])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ - endpoint: 'clips/remove-note', + endpoint: '/clips/remove-note', parameters: { - // @ts-expect-error clipId must not be undefined clipId: aliceClip.id, - // @ts-expect-error noteId must not be undefined noteId: aliceNote.id, ...parameters, }, - user: (user ?? (() => alice))(), + user: (user ?? ((): User => alice))(), }, { status: 400, code: 'INVALID_PARAM', @@ -903,22 +942,21 @@ describe('クリップ', () => { code: 'NO_SUCH_CLIP', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', } }, - { label: '他人のPrivateなクリップから', user: () => bob, assertion: { + { label: '他人のPrivateなクリップから', user: (): object => bob, assertion: { code: 'NO_SUCH_CLIP', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', } }, - { label: '未認証でPrivateなクリップから', user: () => undefined, assertion: { + { label: '未認証でPrivateなクリップから', user: (): undefined => undefined, assertion: { code: 'NO_SUCH_CLIP', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', } }, ])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({ - endpoint: 'clips/notes', + endpoint: '/clips/notes', parameters: { - // @ts-expect-error clipId must not be undefined clipId: aliceClip.id, ...parameters, }, - user: (user ?? (() => alice))(), + user: (user ?? ((): User => alice))(), }, { status: 400, code: 'INVALID_PARAM', diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts deleted file mode 100644 index f023caec5c..0000000000 --- a/packages/backend/test/e2e/drive.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js'; -import type * as misskey from 'cherrypick-js'; - -describe('Drive', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - - beforeAll(async () => { - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - }, 1000 * 60 * 2); - - test('ファイルURLからアップロードできる', async () => { - // utils.js uploadUrl の処理だがAPIレスポンスも見るためここで同様の処理を書いている - - const marker = Math.random().toString(); - - const url = 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/192.jpg'; - - const catcher = makeStreamCatcher( - alice, - 'main', - (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, - (msg) => msg.body.file, - 10 * 1000); - - const res = await api('drive/files/upload-from-url', { - url, - marker, - force: true, - }, alice); - - const file = await catcher; - - assert.strictEqual(res.status, 204); - assert.strictEqual(file.name, '192.jpg'); - assert.strictEqual(file.type, 'image/jpeg'); - }); - - test('ローカルからアップロードできる', async () => { - // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする - - const res = await uploadFile(alice, { path: '192.jpg', name: 'テスト画像' }); - - assert.strictEqual(res.body?.name, 'テスト画像.jpg'); - assert.strictEqual(res.body.type, 'image/jpeg'); - }); - - test('添付ノート一覧を取得できる', async () => { - const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id); - - const note0 = await post(alice, { fileIds: [ids[0]] }); - const note1 = await post(alice, { fileIds: [ids[0], ids[1]] }); - - const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice); - assert.strictEqual(attached0.body.length, 2); - assert.strictEqual(attached0.body[0].id, note1.id); - assert.strictEqual(attached0.body[1].id, note0.id); - - const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice); - assert.strictEqual(attached1.body.length, 1); - assert.strictEqual(attached1.body[0].id, note1.id); - - const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice); - assert.strictEqual(attached2.body.length, 0); - }); - - test('添付ノート一覧は他の人から見えない', async () => { - const file = await uploadFile(alice); - - await post(alice, { fileIds: [file.body!.id] }); - - const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob); - assert.strictEqual(res.status, 400); - assert.strictEqual('error' in res.body, true); - }); -}); diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index f9259ea27b..fb6b442516 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,22 +10,30 @@ import * as assert from 'assert'; // https://github.com/node-fetch/node-fetch/pull/1664 import { Blob } from 'node-fetch'; import { MiUser } from '@/models/_.js'; -import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js'; +import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('Endpoints', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; - let dave: misskey.entities.SignupResponse; + let app: INestApplicationContext; + + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; + let dave: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); dave = await signup({ username: 'dave' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + describe('signup', () => { test('不正なユーザー名でアカウントが作成できない', async () => { const res = await api('signup', { @@ -79,7 +87,6 @@ describe('Endpoints', () => { test('クエリをインジェクションできない', async () => { const res = await api('signin', { username: 'test1', - // @ts-expect-error password must be string password: { $gt: '', }, @@ -104,7 +111,7 @@ describe('Endpoints', () => { const myLocation = '七森中'; const myBirthday = '2000-09-07'; - const res = await api('i/update', { + const res = await api('/i/update', { name: myName, location: myLocation, birthday: myBirthday, @@ -117,29 +124,20 @@ describe('Endpoints', () => { assert.strictEqual(res.body.birthday, myBirthday); }); - test('名前を空白のみにした場合nullになる', async () => { - const res = await api('i/update', { + test('名前を空白にできる', async () => { + const res = await api('/i/update', { name: ' ', }, alice); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.name, null); - }); - - test('名前の前後に空白(ホワイトスペース)を入れてもトリムされる', async () => { - const res = await api('i/update', { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#white_space - name: ' あ い う \u0009\u000b\u000c\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff', - }, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.name, 'あ い う'); + assert.strictEqual(res.body.name, ' '); }); test('誕生日の設定を削除できる', async () => { - await api('i/update', { + await api('/i/update', { birthday: '2000-09-07', }, alice); - const res = await api('i/update', { + const res = await api('/i/update', { birthday: null, }, alice); @@ -149,7 +147,7 @@ describe('Endpoints', () => { }); test('不正な誕生日の形式で怒られる', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { birthday: '2000/09/07', }, alice); assert.strictEqual(res.status, 400); @@ -158,24 +156,24 @@ describe('Endpoints', () => { describe('users/show', () => { test('ユーザーが取得できる', async () => { - const res = await api('users/show', { + const res = await api('/users/show', { userId: alice.id, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual((res.body as unknown as { id: string }).id, alice.id); + assert.strictEqual(res.body.id, alice.id); }); test('ユーザーが存在しなかったら怒る', async () => { - const res = await api('users/show', { + const res = await api('/users/show', { userId: '000000000000000000000000', }); assert.strictEqual(res.status, 404); }); test('間違ったIDで怒られる', async () => { - const res = await api('users/show', { + const res = await api('/users/show', { userId: 'kyoppie', }); assert.strictEqual(res.status, 404); @@ -188,7 +186,7 @@ describe('Endpoints', () => { text: 'test', }); - const res = await api('notes/show', { + const res = await api('/notes/show', { noteId: myPost.id, }, alice); @@ -199,14 +197,14 @@ describe('Endpoints', () => { }); test('投稿が存在しなかったら怒る', async () => { - const res = await api('notes/show', { + const res = await api('/notes/show', { noteId: '000000000000000000000000', }); assert.strictEqual(res.status, 400); }); test('間違ったIDで怒られる', async () => { - const res = await api('notes/show', { + const res = await api('/notes/show', { noteId: 'kyoppie', }); assert.strictEqual(res.status, 400); @@ -217,14 +215,14 @@ describe('Endpoints', () => { test('リアクションできる', async () => { const bobPost = await post(bob, { text: 'hi' }); - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: bobPost.id, reaction: '🚀', }, alice); assert.strictEqual(res.status, 204); - const resNote = await api('notes/show', { + const resNote = await api('/notes/show', { noteId: bobPost.id, }, alice); @@ -235,7 +233,7 @@ describe('Endpoints', () => { test('自分の投稿にもリアクションできる', async () => { const myPost = await post(alice, { text: 'hi' }); - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: myPost.id, reaction: '🚀', }, alice); @@ -246,19 +244,19 @@ describe('Endpoints', () => { test('二重にリアクションすると上書きされる', async () => { const bobPost = await post(bob, { text: 'hi' }); - await api('notes/reactions/create', { + await api('/notes/reactions/create', { noteId: bobPost.id, reaction: '🥰', }, alice); - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: bobPost.id, reaction: '🚀', }, alice); assert.strictEqual(res.status, 204); - const resNote = await api('notes/show', { + const resNote = await api('/notes/show', { noteId: bobPost.id, }, alice); @@ -267,7 +265,7 @@ describe('Endpoints', () => { }); test('存在しない投稿にはリアクションできない', async () => { - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: '000000000000000000000000', reaction: '🚀', }, alice); @@ -275,77 +273,14 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 400); }); - test('リノートにリアクションできない', async () => { - const bobNote = await post(bob, { text: 'hi' }); - const bobRenote = await post(bob, { renoteId: bobNote.id }); - - const res = await api('notes/reactions/create', { - noteId: bobRenote.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.code, 'CANNOT_REACT_TO_RENOTE'); - }); - - test('引用にリアクションできる', async () => { - const bobNote = await post(bob, { text: 'hi' }); - const bobRenote = await post(bob, { text: 'hi again', renoteId: bobNote.id }); - - const res = await api('notes/reactions/create', { - noteId: bobRenote.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 204); - }); - - test('空文字列のリアクションは\u2764にフォールバックされる', async () => { - const bobNote = await post(bob, { text: 'hi' }); - - const res = await api('notes/reactions/create', { - noteId: bobNote.id, - reaction: '', - }, alice); - - assert.strictEqual(res.status, 204); - - const reaction = await api('notes/reactions', { - noteId: bobNote.id, - }); - - assert.strictEqual(reaction.body.length, 1); - assert.strictEqual(reaction.body[0].type, '\u2764'); - }); - - test('絵文字ではない文字列のリアクションは\u2764にフォールバックされる', async () => { - const bobNote = await post(bob, { text: 'hi' }); - - const res = await api('notes/reactions/create', { - noteId: bobNote.id, - reaction: 'Hello!', - }, alice); - - assert.strictEqual(res.status, 204); - - const reaction = await api('notes/reactions', { - noteId: bobNote.id, - }); - - assert.strictEqual(reaction.body.length, 1); - assert.strictEqual(reaction.body[0].type, '\u2764'); - }); - test('空のパラメータで怒られる', async () => { - // @ts-expect-error param must not be empty - const res = await api('notes/reactions/create', {}, alice); + const res = await api('/notes/reactions/create', {}, alice); assert.strictEqual(res.status, 400); }); test('間違ったIDで怒られる', async () => { - const res = await api('notes/reactions/create', { + const res = await api('/notes/reactions/create', { noteId: 'kyoppie', reaction: '🚀', }, alice); @@ -356,7 +291,7 @@ describe('Endpoints', () => { describe('following/create', () => { test('フォローできる', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: alice.id, }, bob); @@ -374,7 +309,7 @@ describe('Endpoints', () => { }); test('既にフォローしている場合は怒る', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: alice.id, }, bob); @@ -382,7 +317,7 @@ describe('Endpoints', () => { }); test('存在しないユーザーはフォローできない', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: '000000000000000000000000', }, alice); @@ -390,7 +325,7 @@ describe('Endpoints', () => { }); test('自分自身はフォローできない', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: alice.id, }, alice); @@ -398,14 +333,13 @@ describe('Endpoints', () => { }); test('空のパラメータで怒られる', async () => { - // @ts-expect-error params must not be empty - const res = await api('following/create', {}, alice); + const res = await api('/following/create', {}, alice); assert.strictEqual(res.status, 400); }); test('間違ったIDで怒られる', async () => { - const res = await api('following/create', { + const res = await api('/following/create', { userId: 'foo', }, alice); @@ -415,11 +349,11 @@ describe('Endpoints', () => { describe('following/delete', () => { test('フォロー解除できる', async () => { - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: alice.id, }, bob); @@ -437,7 +371,7 @@ describe('Endpoints', () => { }); test('フォローしていない場合は怒る', async () => { - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: alice.id, }, bob); @@ -445,7 +379,7 @@ describe('Endpoints', () => { }); test('存在しないユーザーはフォロー解除できない', async () => { - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: '000000000000000000000000', }, alice); @@ -453,7 +387,7 @@ describe('Endpoints', () => { }); test('自分自身はフォロー解除できない', async () => { - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: alice.id, }, alice); @@ -461,14 +395,13 @@ describe('Endpoints', () => { }); test('空のパラメータで怒られる', async () => { - // @ts-expect-error params must not be empty - const res = await api('following/delete', {}, alice); + const res = await api('/following/delete', {}, alice); assert.strictEqual(res.status, 400); }); test('間違ったIDで怒られる', async () => { - const res = await api('following/delete', { + const res = await api('/following/delete', { userId: 'kyoppie', }, alice); @@ -478,20 +411,20 @@ describe('Endpoints', () => { describe('channels/search', () => { test('空白検索で一覧を取得できる', async () => { - await api('channels/create', { + await api('/channels/create', { name: 'aaa', description: 'bbb', }, bob); - await api('channels/create', { + await api('/channels/create', { name: 'ccc1', description: 'ddd1', }, bob); - await api('channels/create', { + await api('/channels/create', { name: 'ccc2', description: 'ddd2', }, bob); - const res = await api('channels/search', { + const res = await api('/channels/search', { query: '', }, bob); @@ -500,7 +433,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body.length, 3); }); test('名前のみの検索で名前を検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'aaa', type: 'nameOnly', }, bob); @@ -511,7 +444,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].name, 'aaa'); }); test('名前のみの検索で名前を複数検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ccc', type: 'nameOnly', }, bob); @@ -521,7 +454,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body.length, 2); }); test('名前のみの検索で説明は検索できない', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'bbb', type: 'nameOnly', }, bob); @@ -531,7 +464,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body.length, 0); }); test('名前と説明の検索で名前を検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ccc1', }, bob); @@ -541,7 +474,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].name, 'ccc1'); }); test('名前と説明での検索で説明を検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ddd1', }, bob); @@ -551,7 +484,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].name, 'ccc1'); }); test('名前と説明の検索で名前を複数検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ccc', }, bob); @@ -560,7 +493,7 @@ describe('Endpoints', () => { assert.strictEqual(res.body.length, 2); }); test('名前と説明での検索で説明を複数検索できる', async () => { - const res = await api('channels/search', { + const res = await api('/channels/search', { query: 'ddd', }, bob); @@ -581,7 +514,7 @@ describe('Endpoints', () => { await uploadFile(alice, { blob: new Blob([new Uint8Array(1024)]), }); - const res = await api('drive', {}, alice); + const res = await api('/drive', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); expect(res.body).toHaveProperty('usage', 1792); @@ -594,7 +527,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, '192.jpg'); + assert.strictEqual(res.body.name, 'Lenna.jpg'); }); test('ファイルに名前を付けられる', async () => { @@ -602,7 +535,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, 'Belmond.jpg'); + assert.strictEqual(res.body.name, 'Belmond.jpg'); }); test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => { @@ -610,12 +543,11 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, 'Belmond.png.jpg'); + assert.strictEqual(res.body.name, 'Belmond.png.jpg'); }); test('ファイル無しで怒られる', async () => { - // @ts-expect-error params must not be empty - const res = await api('drive/files/create', {}, alice); + const res = await api('/drive/files/create', {}, alice); assert.strictEqual(res.status, 400); }); @@ -625,14 +557,14 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, 'image.svg'); - assert.strictEqual(res.body!.type, 'image/svg+xml'); + assert.strictEqual(res.body.name, 'image.svg'); + assert.strictEqual(res.body.type, 'image/svg+xml'); }); for (const type of ['webp', 'avif']) { const mediaType = `image/${type}`; - const getWebpublicType = async (user: misskey.entities.SignupResponse, fileId: string): Promise => { + const getWebpublicType = async (user: any, fileId: string): Promise => { // drive/files/create does not expose webpublicType directly, so get it by posting it const res = await post(user, { text: mediaType, @@ -649,10 +581,10 @@ describe('Endpoints', () => { const res = await uploadFile(alice, { path }); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body!.name, path); - assert.strictEqual(res.body!.type, mediaType); + assert.strictEqual(res.body.name, path); + assert.strictEqual(res.body.type, mediaType); - const webpublicType = await getWebpublicType(alice, res.body!.id); + const webpublicType = await getWebpublicType(alice, res.body.id); assert.strictEqual(webpublicType, 'image/webp'); }); @@ -660,10 +592,10 @@ describe('Endpoints', () => { const path = `without-alpha.${type}`; const res = await uploadFile(alice, { path }); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body!.name, path); - assert.strictEqual(res.body!.type, mediaType); + assert.strictEqual(res.body.name, path); + assert.strictEqual(res.body.type, mediaType); - const webpublicType = await getWebpublicType(alice, res.body!.id); + const webpublicType = await getWebpublicType(alice, res.body.id); assert.strictEqual(webpublicType, 'image/webp'); }); } @@ -674,8 +606,8 @@ describe('Endpoints', () => { const file = (await uploadFile(alice)).body; const newName = 'いちごパスタ.png'; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, name: newName, }, alice); @@ -687,8 +619,8 @@ describe('Endpoints', () => { test('他人のファイルは更新できない', async () => { const file = (await uploadFile(alice)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, name: 'いちごパスタ.png', }, bob); @@ -697,12 +629,12 @@ describe('Endpoints', () => { test('親フォルダを更新できる', async () => { const file = (await uploadFile(alice)).body; - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: folder.id, }, alice); @@ -714,17 +646,17 @@ describe('Endpoints', () => { test('親フォルダを無しにできる', async () => { const file = (await uploadFile(alice)).body; - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - await api('drive/files/update', { - fileId: file!.id, + await api('/drive/files/update', { + fileId: file.id, folderId: folder.id, }, alice); - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: null, }, alice); @@ -735,12 +667,12 @@ describe('Endpoints', () => { test('他人のフォルダには入れられない', async () => { const file = (await uploadFile(alice)).body; - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, bob)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: folder.id, }, alice); @@ -750,8 +682,8 @@ describe('Endpoints', () => { test('存在しないフォルダで怒られる', async () => { const file = (await uploadFile(alice)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: '000000000000000000000000', }, alice); @@ -761,8 +693,8 @@ describe('Endpoints', () => { test('不正なフォルダIDで怒られる', async () => { const file = (await uploadFile(alice)).body; - const res = await api('drive/files/update', { - fileId: file!.id, + const res = await api('/drive/files/update', { + fileId: file.id, folderId: 'foo', }, alice); @@ -770,7 +702,7 @@ describe('Endpoints', () => { }); test('ファイルが存在しなかったら怒る', async () => { - const res = await api('drive/files/update', { + const res = await api('/drive/files/update', { fileId: '000000000000000000000000', name: 'いちごパスタ.png', }, alice); @@ -778,20 +710,8 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 400); }); - test('不正なファイル名で怒られる', async () => { - const file = (await uploadFile(alice)).body; - const newName = ''; - - const res = await api('drive/files/update', { - fileId: file!.id, - name: newName, - }, alice); - - assert.strictEqual(res.status, 400); - }); - test('間違ったIDで怒られる', async () => { - const res = await api('drive/files/update', { + const res = await api('/drive/files/update', { fileId: 'kyoppie', name: 'いちごパスタ.png', }, alice); @@ -802,7 +722,7 @@ describe('Endpoints', () => { describe('drive/folders/create', () => { test('フォルダを作成できる', async () => { - const res = await api('drive/folders/create', { + const res = await api('/drive/folders/create', { name: 'test', }, alice); @@ -814,11 +734,11 @@ describe('Endpoints', () => { describe('drive/folders/update', () => { test('名前を更新できる', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, name: 'new name', }, alice); @@ -829,11 +749,11 @@ describe('Endpoints', () => { }); test('他人のフォルダを更新できない', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, bob)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, name: 'new name', }, alice); @@ -842,14 +762,14 @@ describe('Endpoints', () => { }); test('親フォルダを更新できる', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const parentFolder = (await api('drive/folders/create', { + const parentFolder = (await api('/drive/folders/create', { name: 'parent', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: parentFolder.id, }, alice); @@ -860,18 +780,18 @@ describe('Endpoints', () => { }); test('親フォルダを無しに更新できる', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const parentFolder = (await api('drive/folders/create', { + const parentFolder = (await api('/drive/folders/create', { name: 'parent', }, alice)).body; - await api('drive/folders/update', { + await api('/drive/folders/update', { folderId: folder.id, parentId: parentFolder.id, }, alice); - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: null, }, alice); @@ -882,14 +802,14 @@ describe('Endpoints', () => { }); test('他人のフォルダを親フォルダに設定できない', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const parentFolder = (await api('drive/folders/create', { + const parentFolder = (await api('/drive/folders/create', { name: 'parent', }, bob)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: parentFolder.id, }, alice); @@ -898,18 +818,18 @@ describe('Endpoints', () => { }); test('フォルダが循環するような構造にできない', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const parentFolder = (await api('drive/folders/create', { + const parentFolder = (await api('/drive/folders/create', { name: 'parent', }, alice)).body; - await api('drive/folders/update', { + await api('/drive/folders/update', { folderId: parentFolder.id, parentId: folder.id, }, alice); - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: parentFolder.id, }, alice); @@ -918,25 +838,25 @@ describe('Endpoints', () => { }); test('フォルダが循環するような構造にできない(再帰的)', async () => { - const folderA = (await api('drive/folders/create', { + const folderA = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const folderB = (await api('drive/folders/create', { + const folderB = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const folderC = (await api('drive/folders/create', { + const folderC = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - await api('drive/folders/update', { + await api('/drive/folders/update', { folderId: folderB.id, parentId: folderA.id, }, alice); - await api('drive/folders/update', { + await api('/drive/folders/update', { folderId: folderC.id, parentId: folderB.id, }, alice); - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folderA.id, parentId: folderC.id, }, alice); @@ -945,11 +865,11 @@ describe('Endpoints', () => { }); test('フォルダが循環するような構造にできない(自身)', async () => { - const folderA = (await api('drive/folders/create', { + const folderA = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folderA.id, parentId: folderA.id, }, alice); @@ -958,11 +878,11 @@ describe('Endpoints', () => { }); test('存在しない親フォルダを設定できない', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: '000000000000000000000000', }, alice); @@ -971,11 +891,11 @@ describe('Endpoints', () => { }); test('不正な親フォルダIDで怒られる', async () => { - const folder = (await api('drive/folders/create', { + const folder = (await api('/drive/folders/create', { name: 'test', }, alice)).body; - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: folder.id, parentId: 'foo', }, alice); @@ -984,7 +904,7 @@ describe('Endpoints', () => { }); test('存在しないフォルダを更新できない', async () => { - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: '000000000000000000000000', }, alice); @@ -992,7 +912,7 @@ describe('Endpoints', () => { }); test('不正なフォルダIDで怒られる', async () => { - const res = await api('drive/folders/update', { + const res = await api('/drive/folders/update', { folderId: 'foo', }, alice); @@ -1002,7 +922,7 @@ describe('Endpoints', () => { describe('messaging/messages/create', () => { test('メッセージを送信できる', async () => { - const res = await api('messaging/messages/create', { + const res = await api('/messaging/messages/create', { userId: bob.id, text: 'test', }, alice); @@ -1013,7 +933,7 @@ describe('Endpoints', () => { }); test('自分自身にはメッセージを送信できない', async () => { - const res = await api('messaging/messages/create', { + const res = await api('/messaging/messages/create', { userId: alice.id, text: 'Yo', }, alice); @@ -1022,7 +942,7 @@ describe('Endpoints', () => { }); test('存在しないユーザーにはメッセージを送信できない', async () => { - const res = await api('messaging/messages/create', { + const res = await api('/messaging/messages/create', { userId: '000000000000000000000000', text: 'test', }, alice); @@ -1031,7 +951,7 @@ describe('Endpoints', () => { }); test('不正なユーザーIDで怒られる', async () => { - const res = await api('messaging/messages/create', { + const res = await api('/messaging/messages/create', { userId: 'foo', text: 'test', }, alice); @@ -1040,7 +960,7 @@ describe('Endpoints', () => { }); test('テキストが無くて怒られる', async () => { - const res = await api('messaging/messages/create', { + const res = await api('/messaging/messages/create', { userId: bob.id, }, alice); @@ -1048,7 +968,7 @@ describe('Endpoints', () => { }); test('文字数オーバーで怒られる', async () => { - const res = await api('messaging/messages/create', { + const res = await api('/messaging/messages/create', { userId: bob.id, text: '!'.repeat(3001), }, alice); @@ -1070,7 +990,7 @@ describe('Endpoints', () => { visibleUserIds: [alice.id], }); - const res = await api('notes/replies', { + const res = await api('/notes/replies', { noteId: alicePost.id, }, carol); @@ -1082,7 +1002,7 @@ describe('Endpoints', () => { describe('notes/timeline', () => { test('フォロワー限定投稿が含まれる', async () => { - await api('following/create', { + await api('/following/create', { userId: carol.id, }, dave); @@ -1091,7 +1011,7 @@ describe('Endpoints', () => { visibility: 'followers', }); - const res = await api('notes/timeline', {}, dave); + const res = await api('/notes/timeline', {}, dave); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); @@ -1112,52 +1032,52 @@ describe('Endpoints', () => { test('他者に関するメモを更新できる', async () => { const memo = '10月まで低浮上とのこと。'; - const res1 = await api('users/update-memo', { + const res1 = await api('/users/update-memo', { memo, userId: bob.id, }, alice); - const res2 = await api('users/show', { + const res2 = await api('/users/show', { userId: bob.id, }, alice); assert.strictEqual(res1.status, 204); - assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo); + assert.strictEqual(res2.body?.memo, memo); }); test('自分に関するメモを更新できる', async () => { const memo = 'チケットを月末までに買う。'; - const res1 = await api('users/update-memo', { + const res1 = await api('/users/update-memo', { memo, userId: alice.id, }, alice); - const res2 = await api('users/show', { + const res2 = await api('/users/show', { userId: alice.id, }, alice); assert.strictEqual(res1.status, 204); - assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo); + assert.strictEqual(res2.body?.memo, memo); }); test('メモを削除できる', async () => { const memo = '10月まで低浮上とのこと。'; - await api('users/update-memo', { + await api('/users/update-memo', { memo, userId: bob.id, }, alice); - await api('users/update-memo', { + await api('/users/update-memo', { memo: '', userId: bob.id, }, alice); - const res = await api('users/show', { + const res = await api('/users/show', { userId: bob.id, }, alice); // memoには常に文字列かnullが入っている(5cac151) - assert.strictEqual((res.body as unknown as { memo: string | null }).memo, null); + assert.strictEqual(res.body.memo, null); }); test('メモは個人ごとに独立して保存される', async () => { @@ -1165,27 +1085,27 @@ describe('Endpoints', () => { const memoCarolToBob = '例の件について今度問いただす。'; await Promise.all([ - api('users/update-memo', { + api('/users/update-memo', { memo: memoAliceToBob, userId: bob.id, }, alice), - api('users/update-memo', { + api('/users/update-memo', { memo: memoCarolToBob, userId: bob.id, }, carol), ]); const [resAlice, resCarol] = await Promise.all([ - api('users/show', { + api('/users/show', { userId: bob.id, }, alice), - api('users/show', { + api('/users/show', { userId: bob.id, }, carol), ]); - assert.strictEqual((resAlice.body as unknown as { memo: string }).memo, memoAliceToBob); - assert.strictEqual((resCarol.body as unknown as { memo: string }).memo, memoCarolToBob); + assert.strictEqual(resAlice.body.memo, memoAliceToBob); + assert.strictEqual(resCarol.body.memo, memoCarolToBob); }); }); }); diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts deleted file mode 100644 index d59b6107d9..0000000000 --- a/packages/backend/test/e2e/exports.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { api, port, post, signup, startJobQueue } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; -import type * as misskey from 'cherrypick-js'; - -describe('export-clips', () => { - let queue: INestApplicationContext; - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - - // XXX: Any better way to get the result? - async function pollFirstDriveFile() { - while (true) { - const files = (await api('drive/files', {}, alice)).body; - if (!files.length) { - await new Promise(r => setTimeout(r, 100)); - continue; - } - if (files.length > 1) { - throw new Error('Too many files?'); - } - const file = (await api('drive/files/show', { fileId: files[0].id }, alice)).body; - const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`)); - return await res.json(); - } - } - - beforeAll(async () => { - queue = await startJobQueue(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - }, 1000 * 60 * 2); - - afterAll(async () => { - await queue.close(); - }); - - beforeEach(async () => { - // Clean all clips and files of alice - const clips = (await api('clips/list', {}, alice)).body; - for (const clip of clips) { - const res = await api('clips/delete', { clipId: clip.id }, alice); - if (res.status !== 204) { - throw new Error('Failed to delete clip'); - } - } - const files = (await api('drive/files', {}, alice)).body; - for (const file of files) { - const res = await api('drive/files/delete', { fileId: file.id }, alice); - if (res.status !== 204) { - throw new Error('Failed to delete file'); - } - } - }); - - test('basic export', async () => { - const res1 = await api('clips/create', { - name: 'foo', - description: 'bar', - }, alice); - assert.strictEqual(res1.status, 200); - - const res2 = await api('i/export-clips', {}, alice); - assert.strictEqual(res2.status, 204); - - const exported = await pollFirstDriveFile(); - assert.strictEqual(exported[0].name, 'foo'); - assert.strictEqual(exported[0].description, 'bar'); - assert.strictEqual(exported[0].clipNotes.length, 0); - }); - - test('export with notes', async () => { - const res = await api('clips/create', { - name: 'foo', - description: 'bar', - }, alice); - assert.strictEqual(res.status, 200); - const clip = res.body; - - const note1 = await post(alice, { - text: 'baz1', - }); - - const note2 = await post(alice, { - text: 'baz2', - poll: { - choices: ['sakura', 'izumi', 'ako'], - }, - }); - - for (const note of [note1, note2]) { - const res2 = await api('clips/add-note', { - clipId: clip.id, - noteId: note.id, - }, alice); - assert.strictEqual(res2.status, 204); - } - - const res3 = await api('i/export-clips', {}, alice); - assert.strictEqual(res3.status, 204); - - const exported = await pollFirstDriveFile(); - assert.strictEqual(exported[0].name, 'foo'); - assert.strictEqual(exported[0].description, 'bar'); - assert.strictEqual(exported[0].clipNotes.length, 2); - assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); - assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2'); - assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura'); - }); - - test('multiple clips', async () => { - const res1 = await api('clips/create', { - name: 'kawaii', - description: 'kawaii', - }, alice); - assert.strictEqual(res1.status, 200); - const clip1 = res1.body; - - const res2 = await api('clips/create', { - name: 'yuri', - description: 'yuri', - }, alice); - assert.strictEqual(res2.status, 200); - const clip2 = res2.body; - - const note1 = await post(alice, { - text: 'baz1', - }); - - const note2 = await post(alice, { - text: 'baz2', - }); - - { - const res = await api('clips/add-note', { - clipId: clip1.id, - noteId: note1.id, - }, alice); - assert.strictEqual(res.status, 204); - } - - { - const res = await api('clips/add-note', { - clipId: clip2.id, - noteId: note2.id, - }, alice); - assert.strictEqual(res.status, 204); - } - - { - const res = await api('i/export-clips', {}, alice); - assert.strictEqual(res.status, 204); - } - - const exported = await pollFirstDriveFile(); - assert.strictEqual(exported[0].name, 'kawaii'); - assert.strictEqual(exported[0].clipNotes.length, 1); - assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1'); - assert.strictEqual(exported[1].name, 'yuri'); - assert.strictEqual(exported[1].clipNotes.length, 1); - assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2'); - }); - - test('Clipping other user\'s note', async () => { - const res = await api('clips/create', { - name: 'kawaii', - description: 'kawaii', - }, alice); - assert.strictEqual(res.status, 200); - const clip = res.body; - - const note = await post(bob, { - text: 'baz', - visibility: 'followers', - }); - - const res2 = await api('clips/add-note', { - clipId: clip.id, - noteId: note.id, - }, alice); - assert.strictEqual(res2.status, 204); - - const res3 = await api('i/export-clips', {}, alice); - assert.strictEqual(res3.status, 204); - - const exported = await pollFirstDriveFile(); - assert.strictEqual(exported[0].name, 'kawaii'); - assert.strictEqual(exported[0].clipNotes.length, 1); - assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz'); - assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob'); - }); -}); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 5c5a9c1f1a..fb34b60e37 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -1,13 +1,14 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; +import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; // Request Accept @@ -22,16 +23,18 @@ const HTML = 'text/html; charset=utf-8'; const JSON_UTF8 = 'application/json; charset=utf-8'; describe('Webリソース', () => { - let alice: misskey.entities.SignupResponse; - let aliceUploadedFile: misskey.entities.DriveFile | null; - let alicesPost: misskey.entities.Note; - let alicePage: misskey.entities.Page; - let alicePlay: misskey.entities.Flash; - let aliceClip: misskey.entities.Clip; - let aliceGalleryPost: misskey.entities.GalleryPost; - let aliceChannel: misskey.entities.Channel; + let app: INestApplicationContext; - let bob: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let aliceUploadedFile: any; + let alicesPost: any; + let alicePage: any; + let alicePlay: any; + let aliceClip: any; + let aliceGalleryPost: any; + let aliceChannel: any; + + let bob: misskey.entities.MeSignup; type Request = { path: string, @@ -76,8 +79,9 @@ describe('Webリソース', () => { }; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); - aliceUploadedFile = (await uploadFile(alice)).body; + aliceUploadedFile = await uploadFile(alice); alicesPost = await post(alice, { text: 'test', }); @@ -85,13 +89,17 @@ describe('Webリソース', () => { alicePlay = await play(alice, {}); aliceClip = await clip(alice, {}); aliceGalleryPost = await galleryPost(alice, { - fileIds: [aliceUploadedFile!.id], + fileIds: [aliceUploadedFile.body.id], }); aliceChannel = await channel(alice, {}); bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + describe.each([ { path: '/', type: HTML }, { path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。" @@ -153,23 +161,6 @@ describe('Webリソース', () => { path: path('nonexisting'), status: 404, })); - - describe(' has entry such ', () => { - beforeEach(() => { - post(alice, { text: "**a**" }) - }); - - test('MFMを含まない。', async () => { - const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text()); - const _body: unknown = content.body; - // JSONフィードのときは改めて文字列化する - const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string; - - if (body.includes("**a**")) { - throw new Error("MFM shouldn't be included"); - } - }); - }) }); describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { diff --git a/packages/backend/test/e2e/fetch-validate-ap-deny.ts b/packages/backend/test/e2e/fetch-validate-ap-deny.ts deleted file mode 100644 index 75d60b40bd..0000000000 --- a/packages/backend/test/e2e/fetch-validate-ap-deny.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -process.env.NODE_ENV = 'test'; - -import { validateContentTypeSetAsActivityPub, validateContentTypeSetAsJsonLD } from '@/core/activitypub/misc/validator.js'; -import { signup, uploadFile, relativeFetch } from '../utils.js'; -import type * as misskey from 'cherrypick-js'; - -describe('validateContentTypeSetAsActivityPub/JsonLD (deny case)', () => { - let alice: misskey.entities.SignupResponse; - let aliceUploadedFile: any; - - beforeAll(async () => { - alice = await signup({ username: 'alice' }); - aliceUploadedFile = await uploadFile(alice); - }, 1000 * 60 * 2); - - test('ActivityStreams: ファイルはエラーになる', async () => { - const res = await relativeFetch(aliceUploadedFile.webpublicUrl); - - function doValidate() { - validateContentTypeSetAsActivityPub(res); - } - - expect(doValidate).toThrow('Content type is not'); - }); - - test('JSON-LD: ファイルはエラーになる', async () => { - const res = await relativeFetch(aliceUploadedFile.webpublicUrl); - - function doValidate() { - validateContentTypeSetAsJsonLD(res); - } - - expect(doValidate).toThrow('Content type is not'); - }); -}); diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 96099d3df2..6f3a22de90 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -1,33 +1,41 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, signup, simpleGet } from '../utils.js'; +import { signup, api, startServer, simpleGet } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('FF visibility', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; + let app: INestApplicationContext; + + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'public', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -39,36 +47,36 @@ describe('FF visibility', () => { test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'public', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 200); assert.strictEqual(Array.isArray(followingRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 200); assert.strictEqual(Array.isArray(followingRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 200); @@ -78,36 +86,36 @@ describe('FF visibility', () => { test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'public', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 200); assert.strictEqual(Array.isArray(followersRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'public', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 200); assert.strictEqual(Array.isArray(followersRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'public', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 200); @@ -116,15 +124,15 @@ describe('FF visibility', () => { }); test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); @@ -136,36 +144,36 @@ describe('FF visibility', () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'public', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); assert.strictEqual(followingRes.status, 200); assert.strictEqual(Array.isArray(followingRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); assert.strictEqual(followingRes.status, 200); assert.strictEqual(Array.isArray(followingRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); assert.strictEqual(followingRes.status, 200); @@ -175,36 +183,36 @@ describe('FF visibility', () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'followers', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); assert.strictEqual(followersRes.status, 200); assert.strictEqual(Array.isArray(followersRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); assert.strictEqual(followersRes.status, 200); assert.strictEqual(Array.isArray(followersRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'followers', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); assert.strictEqual(followersRes.status, 200); @@ -213,15 +221,15 @@ describe('FF visibility', () => { }); test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -231,34 +239,34 @@ describe('FF visibility', () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'public', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 400); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 400); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 400); @@ -267,34 +275,34 @@ describe('FF visibility', () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'followers', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 400); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 400); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'followers', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 400); @@ -302,19 +310,19 @@ describe('FF visibility', () => { }); test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -326,45 +334,45 @@ describe('FF visibility', () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'public', }, alice); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 200); assert.strictEqual(Array.isArray(followingRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 200); assert.strictEqual(Array.isArray(followingRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'private', }, alice); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 200); @@ -374,45 +382,45 @@ describe('FF visibility', () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'followers', }, alice); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 200); assert.strictEqual(Array.isArray(followersRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'followers', }, alice); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 200); assert.strictEqual(Array.isArray(followersRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'followers', }, alice); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 200); @@ -421,15 +429,15 @@ describe('FF visibility', () => { }); test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); @@ -441,36 +449,36 @@ describe('FF visibility', () => { test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'public', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); assert.strictEqual(followingRes.status, 200); assert.strictEqual(Array.isArray(followingRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); assert.strictEqual(followingRes.status, 200); assert.strictEqual(Array.isArray(followingRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); assert.strictEqual(followingRes.status, 200); @@ -480,36 +488,36 @@ describe('FF visibility', () => { test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'private', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); assert.strictEqual(followersRes.status, 200); assert.strictEqual(Array.isArray(followersRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'private', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); assert.strictEqual(followersRes.status, 200); assert.strictEqual(Array.isArray(followersRes.body), true); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'private', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); assert.strictEqual(followersRes.status, 200); @@ -518,15 +526,15 @@ describe('FF visibility', () => { }); test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -536,34 +544,34 @@ describe('FF visibility', () => { test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'public', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 400); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'followers', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 400); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'private', }, alice); - const followingRes = await api('users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); assert.strictEqual(followingRes.status, 400); @@ -572,34 +580,34 @@ describe('FF visibility', () => { test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', followersVisibility: 'private', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 400); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', followersVisibility: 'private', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 400); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', followersVisibility: 'private', }, alice); - const followersRes = await api('users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); assert.strictEqual(followersRes.status, 400); @@ -609,7 +617,7 @@ describe('FF visibility', () => { describe('AP', () => { test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => { { - await api('i/update', { + await api('/i/update', { followingVisibility: 'public', }, alice); @@ -617,7 +625,7 @@ describe('FF visibility', () => { assert.strictEqual(followingRes.status, 200); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'followers', }, alice); @@ -625,7 +633,7 @@ describe('FF visibility', () => { assert.strictEqual(followingRes.status, 403); } { - await api('i/update', { + await api('/i/update', { followingVisibility: 'private', }, alice); @@ -636,7 +644,7 @@ describe('FF visibility', () => { test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => { { - await api('i/update', { + await api('/i/update', { followersVisibility: 'public', }, alice); @@ -644,7 +652,7 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 200); } { - await api('i/update', { + await api('/i/update', { followersVisibility: 'followers', }, alice); @@ -652,7 +660,7 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 403); } { - await api('i/update', { + await api('/i/update', { followersVisibility: 'private', }, alice); diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 2aee9039e2..ca3f825f56 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -1,38 +1,37 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { INestApplicationContext } from '@nestjs/common'; - process.env.NODE_ENV = 'test'; -import { setTimeout } from 'node:timers/promises'; import * as assert from 'assert'; import { loadConfig } from '@/config.js'; -import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { MiUser, UsersRepository } from '@/models/_.js'; import { jobQueue } from '@/boot/common.js'; -import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('Account Move', () => { + let app: INestApplicationContext; let jq: INestApplicationContext; let url: URL; - let root: misskey.entities.SignupResponse; - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; - let dave: misskey.entities.SignupResponse; - let eve: misskey.entities.SignupResponse; - let frank: misskey.entities.SignupResponse; + let root: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; + let dave: misskey.entities.MeSignup; + let eve: misskey.entities.MeSignup; + let frank: misskey.entities.MeSignup; let Users: UsersRepository; beforeAll(async () => { + app = await startServer(); jq = await jobQueue(); - const config = loadConfig(); url = new URL(config.url); const connection = await initTestDb(false); @@ -43,11 +42,11 @@ describe('Account Move', () => { dave = await signup({ username: 'dave' }); eve = await signup({ username: 'eve' }); frank = await signup({ username: 'frank' }); - Users = connection.getRepository(MiUser).extend(miRepository as MiRepository); + Users = connection.getRepository(MiUser); }, 1000 * 60 * 2); afterAll(async () => { - await jq.close(); + await Promise.all([app.close(), jq.close()]); }); describe('Create Alias', () => { @@ -56,7 +55,7 @@ describe('Account Move', () => { }, 1000 * 10); test('Able to create an alias', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`], }, bob); @@ -68,7 +67,7 @@ describe('Account Move', () => { }); test('Able to create a local alias without hostname', async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: ['@alice'], }, bob); @@ -78,7 +77,7 @@ describe('Account Move', () => { }); test('Able to create a local alias without @', async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: ['alice'], }, bob); @@ -88,55 +87,55 @@ describe('Account Move', () => { }); test('Able to set remote user (but may fail)', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: ['@syuilo@example.com'], }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER'); - assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Unable to add duplicated aliases to alsoKnownAs', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`], }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'INVALID_PARAM'); - assert.strictEqual(castAsError(res.body).error.id, '3d81ceae-475f-4600-b2a8-2bc116157532'); + assert.strictEqual(res.body.error.code, 'INVALID_PARAM'); + assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532'); }); test('Unable to add itself', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: [`@bob@${url.hostname}`], }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'FORBIDDEN_TO_SET_YOURSELF'); - assert.strictEqual(castAsError(res.body).error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4'); + assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF'); + assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4'); }); test('Unable to add a nonexisting local account to alsoKnownAs', async () => { - const res1 = await api('i/update', { + const res1 = await api('/i/update', { alsoKnownAs: [`@nonexist@${url.hostname}`], }, bob); assert.strictEqual(res1.status, 400); - assert.strictEqual(castAsError(res1.body).error.code, 'NO_SUCH_USER'); - assert.strictEqual(castAsError(res1.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); - const res2 = await api('i/update', { + const res2 = await api('/i/update', { alsoKnownAs: ['@alice', 'nonexist'], }, bob); assert.strictEqual(res2.status, 400); - assert.strictEqual(castAsError(res2.body).error.code, 'NO_SUCH_USER'); - assert.strictEqual(castAsError(res2.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Able to add two existing local account to alsoKnownAs', async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`], }, bob); @@ -147,10 +146,10 @@ describe('Account Move', () => { }); test('Able to properly overwrite alsoKnownAs', async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`], }, bob); - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`], }, bob); @@ -165,171 +164,164 @@ describe('Account Move', () => { let antennaId = ''; beforeAll(async () => { - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`], }, root); - const listRoot = await api('users/lists/create', { + const listRoot = await api('/users/lists/create', { name: secureRndstr(8), }, root); - await api('users/lists/push', { + await api('/users/lists/push', { listId: listRoot.body.id, userId: alice.id, }, root); - await api('following/create', { + await api('/following/create', { userId: root.id, }, alice); - await api('following/create', { + await api('/following/create', { userId: eve.id, }, alice); - const antenna = await api('antennas/create', { + const antenna = await api('/antennas/create', { name: secureRndstr(8), src: 'home', - keywords: [[secureRndstr(8)]], + keywords: [secureRndstr(8)], excludeKeywords: [], users: [], caseSensitive: false, localOnly: false, withReplies: false, withFile: false, + notify: false, }, alice); antennaId = antenna.body.id; - await api('i/update', { + await api('/i/update', { alsoKnownAs: [`@alice@${url.hostname}`], }, bob); - await api('following/create', { + await api('/following/create', { userId: alice.id, }, carol); - await api('mute/create', { + await api('/mute/create', { userId: alice.id, }, dave); - await api('blocking/create', { + await api('/blocking/create', { userId: alice.id, }, dave); - await api('following/create', { + await api('/following/create', { userId: eve.id, }, dave); - await api('following/create', { + await api('/following/create', { userId: dave.id, }, eve); - const listEve = await api('users/lists/create', { + const listEve = await api('/users/lists/create', { name: secureRndstr(8), }, eve); - await api('users/lists/push', { + await api('/users/lists/push', { listId: listEve.body.id, userId: bob.id, }, eve); - await api('i/update', { + await api('/i/update', { isLocked: true, }, frank); - await api('following/create', { + await api('/following/create', { userId: frank.id, }, alice); - await api('following/requests/accept', { + await api('/following/requests/accept', { userId: alice.id, }, frank); }, 1000 * 10); test('Prohibit the root account from moving', async () => { - const res = await api('i/move', { + const res = await api('/i/move', { moveToAccount: `@bob@${url.hostname}`, }, root); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NOT_ROOT_FORBIDDEN'); - assert.strictEqual(castAsError(res.body).error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24'); + assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN'); + assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24'); }); test('Unable to move to a nonexisting local account', async () => { - const res = await api('i/move', { + const res = await api('/i/move', { moveToAccount: `@nonexist@${url.hostname}`, }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER'); - assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); + assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Unable to move if alsoKnownAs is invalid', async () => { - const res = await api('i/move', { + const res = await api('/i/move', { moveToAccount: `@carol@${url.hostname}`, }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS'); - assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); + assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS'); + assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); }); test('Relationships have been properly migrated', async () => { - const move = await api('i/move', { + const move = await api('/i/move', { moveToAccount: `@bob@${url.hostname}`, }, alice); assert.strictEqual(move.status, 200); - await setTimeout(1000 * 3); // wait for jobs to finish + await sleep(1000 * 3); // wait for jobs to finish // Unfollow delayed? - const aliceFollowings = await api('users/following', { + const aliceFollowings = await api('/users/following', { userId: alice.id, }, alice); assert.strictEqual(aliceFollowings.status, 200); - assert.ok(aliceFollowings); assert.strictEqual(aliceFollowings.body.length, 3); - const carolFollowings = await api('users/following', { + const carolFollowings = await api('/users/following', { userId: carol.id, }, carol); assert.strictEqual(carolFollowings.status, 200); - assert.ok(carolFollowings); assert.strictEqual(carolFollowings.body.length, 2); assert.strictEqual(carolFollowings.body[0].followeeId, bob.id); assert.strictEqual(carolFollowings.body[1].followeeId, alice.id); - const blockings = await api('blocking/list', {}, dave); + const blockings = await api('/blocking/list', {}, dave); assert.strictEqual(blockings.status, 200); - assert.ok(blockings); assert.strictEqual(blockings.body.length, 2); assert.strictEqual(blockings.body[0].blockeeId, bob.id); assert.strictEqual(blockings.body[1].blockeeId, alice.id); - const mutings = await api('mute/list', {}, dave); + const mutings = await api('/mute/list', {}, dave); assert.strictEqual(mutings.status, 200); - assert.ok(mutings); assert.strictEqual(mutings.body.length, 2); assert.strictEqual(mutings.body[0].muteeId, bob.id); assert.strictEqual(mutings.body[1].muteeId, alice.id); - const rootLists = await api('users/lists/list', {}, root); + const rootLists = await api('/users/lists/list', {}, root); assert.strictEqual(rootLists.status, 200); - assert.ok(rootLists); - assert.ok(rootLists.body[0].userIds); assert.strictEqual(rootLists.body[0].userIds.length, 2); assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id)); - const eveLists = await api('users/lists/list', {}, eve); + const eveLists = await api('/users/lists/list', {}, eve); assert.strictEqual(eveLists.status, 200); - assert.ok(eveLists); - assert.ok(eveLists.body[0].userIds); assert.strictEqual(eveLists.body[0].userIds.length, 1); assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id)); }); test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => { await successfulApiCall({ - endpoint: 'following/create', + endpoint: '/following/create', parameters: { userId: frank.id, }, user: bob, }); - const followers = await api('users/followers', { + const followers = await api('/users/followers', { userId: frank.id, }, frank); @@ -339,9 +331,9 @@ describe('Account Move', () => { }); test('Unfollowed after 10 sec (24 hours in production).', async () => { - await setTimeout(1000 * 8); + await sleep(1000 * 8); - const following = await api('users/following', { + const following = await api('/users/following', { userId: alice.id, }, alice); @@ -350,17 +342,17 @@ describe('Account Move', () => { }); test('Unable to move if the destination account has already moved.', async () => { - const res = await api('i/move', { + const res = await api('/i/move', { moveToAccount: `@alice@${url.hostname}`, }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS'); - assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); + assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS'); + assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); }); test('Follow and follower counts are properly adjusted', async () => { - await api('following/create', { + await api('/following/create', { userId: alice.id, }, eve); const newAlice = await Users.findOneByOrFail({ id: alice.id }); @@ -373,7 +365,7 @@ describe('Account Move', () => { assert.strictEqual(newEve.followingCount, 1); assert.strictEqual(newEve.followersCount, 1); - await api('following/delete', { + await api('/following/delete', { userId: alice.id, }, eve); newEve = await Users.findOneByOrFail({ id: eve.id }); @@ -382,94 +374,91 @@ describe('Account Move', () => { }); test.each([ - 'antennas/create', - 'channels/create', - 'channels/favorite', - 'channels/follow', - 'channels/unfavorite', - 'channels/unfollow', - 'clips/add-note', - 'clips/create', - 'clips/favorite', - 'clips/remove-note', - 'clips/unfavorite', - 'clips/update', - 'drive/files/upload-from-url', - 'flash/create', - 'flash/like', - 'flash/unlike', - 'flash/update', - 'following/create', - 'gallery/posts/create', - 'gallery/posts/like', - 'gallery/posts/unlike', - 'gallery/posts/update', - 'i/claim-achievement', - 'i/move', - 'i/import-blocking', - 'i/import-following', - 'i/import-muting', - 'i/import-user-lists', - 'i/pin', - 'mute/create', - 'notes/create', - 'notes/favorites/create', - 'notes/polls/vote', - 'notes/reactions/create', - 'pages/create', - 'pages/like', - 'pages/unlike', - 'pages/update', - 'renote-mute/create', - 'users/lists/create', - 'users/lists/pull', - 'users/lists/push', - ] as const)('Prohibit access after moving: %s', async (endpoint) => { + '/antennas/create', + '/channels/create', + '/channels/favorite', + '/channels/follow', + '/channels/unfavorite', + '/channels/unfollow', + '/clips/add-note', + '/clips/create', + '/clips/favorite', + '/clips/remove-note', + '/clips/unfavorite', + '/clips/update', + '/drive/files/upload-from-url', + '/flash/create', + '/flash/like', + '/flash/unlike', + '/flash/update', + '/following/create', + '/gallery/posts/create', + '/gallery/posts/like', + '/gallery/posts/unlike', + '/gallery/posts/update', + '/i/claim-achievement', + '/i/move', + '/i/import-blocking', + '/i/import-following', + '/i/import-muting', + '/i/import-user-lists', + '/i/pin', + '/mute/create', + '/notes/create', + '/notes/favorites/create', + '/notes/polls/vote', + '/notes/reactions/create', + '/pages/create', + '/pages/like', + '/pages/unlike', + '/pages/update', + '/renote-mute/create', + '/users/lists/create', + '/users/lists/pull', + '/users/lists/push', + ])('Prohibit access after moving: %s', async (endpoint) => { const res = await api(endpoint, {}, alice); assert.strictEqual(res.status, 403); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit access after moving: /antennas/update', async () => { - const res = await api('antennas/update', { + const res = await api('/antennas/update', { antennaId, name: secureRndstr(8), src: 'users', - keywords: [[secureRndstr(8)]], + keywords: [secureRndstr(8)], excludeKeywords: [], users: [eve.id], caseSensitive: false, localOnly: false, withReplies: false, withFile: false, + notify: false, }, alice); assert.strictEqual(res.status, 403); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit access after moving: /drive/files/create', async () => { - // FIXME: 一旦逃げておく const res = await uploadFile(alice); assert.strictEqual(res.status, 403); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit updating alsoKnownAs after moving', async () => { - const res = await api('i/update', { + const res = await api('/i/update', { alsoKnownAs: [`@eve@${url.hostname}`], }, alice); assert.strictEqual(res.status, 403); - assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); }); }); diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index 080ea8b69f..2c4e59b5e5 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -1,63 +1,61 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, post, react, signup, waitFire } from '../utils.js'; +import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('Mute', () => { + let app: INestApplicationContext; + // alice mutes carol - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); - - // Mute: alice ==> carol - await api('mute/create', { - userId: carol.id, - }, alice); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('ミュート作成', async () => { - const res = await api('mute/create', { - userId: bob.id, + const res = await api('/mute/create', { + userId: carol.id, }, alice); assert.strictEqual(res.status, 204); - - // 単体でも走らせられるように副作用消す - await api('mute/delete', { - userId: bob.id, - }, alice); }); test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => { const bobNote = await post(bob, { text: '@alice hi' }); const carolNote = await post(carol, { text: '@alice hi' }); - const res = await api('notes/mentions', {}, alice); + const res = await api('/notes/mentions', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { // 状態リセット - await api('i/read-all-unread-notes', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); await post(carol, { text: '@alice hi' }); - const res = await api('i', {}, alice); + const res = await api('/i', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(res.body.hasUnreadMentions, false); @@ -65,7 +63,7 @@ describe('Mute', () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { // 状態リセット - await api('i/read-all-unread-notes', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); @@ -74,8 +72,8 @@ describe('Mute', () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { // 状態リセット - await api('i/read-all-unread-notes', {}, alice); - await api('notifications/mark-all-as-read', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); + await api('/notifications/mark-all-as-read', {}, alice); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); @@ -88,13 +86,13 @@ describe('Mute', () => { const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); - const res = await api('notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => { @@ -104,13 +102,13 @@ describe('Mute', () => { renoteId: carolNote.id, }); - const res = await api('notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); }); @@ -120,201 +118,12 @@ describe('Mute', () => { await react(bob, aliceNote, 'like'); await react(carol, aliceNote, 'like'); - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリプライが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { text: '@alice hi', replyId: aliceNote.id }); - await post(carol, { text: '@alice hi', replyId: aliceNote.id }); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリプライが含まれない', async () => { - await post(alice, { text: 'hi' }); - await post(bob, { text: '@alice hi' }); - await post(carol, { text: '@alice hi' }); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { text: 'hi', renoteId: aliceNote.id }); - await post(carol, { text: 'hi', renoteId: aliceNote.id }); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリノートが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { renoteId: aliceNote.id }); - await post(carol, { renoteId: aliceNote.id }); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { - await api('following/create', { userId: alice.id }, bob); - await api('following/create', { userId: alice.id }, carol); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - - await api('following/delete', { userId: alice.id }, bob); - await api('following/delete', { userId: alice.id }, carol); - }); - - test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => { - await api('i/update', { isLocked: true }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/create', { userId: alice.id }, carol); - - const res = await api('i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - - await api('following/delete', { userId: alice.id }, bob); - await api('following/delete', { userId: alice.id }, carol); - }); - }); - - describe('Notification (Grouped)', () => { - test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await react(bob, aliceNote, 'like'); - await react(carol, aliceNote, 'like'); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - test('通知にミュートしているユーザーからのリプライが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { text: '@alice hi', replyId: aliceNote.id }); - await post(carol, { text: '@alice hi', replyId: aliceNote.id }); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリプライが含まれない', async () => { - await post(alice, { text: 'hi' }); - await post(bob, { text: '@alice hi' }); - await post(carol, { text: '@alice hi' }); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { text: 'hi', renoteId: aliceNote.id }); - await post(carol, { text: 'hi', renoteId: aliceNote.id }); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのリノートが含まれない', async () => { - const aliceNote = await post(alice, { text: 'hi' }); - await post(bob, { renoteId: aliceNote.id }); - await post(carol, { renoteId: aliceNote.id }); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - }); - - test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { - await api('following/create', { userId: alice.id }, bob); - await api('following/create', { userId: alice.id }, carol); - - const res = await api('i/notifications-grouped', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); - - await api('following/delete', { userId: alice.id }, bob); - await api('following/delete', { userId: alice.id }, carol); - }); - - test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => { - await api('i/update', { isLocked: true }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/create', { userId: alice.id }, carol); - - const res = await api('i/notifications-grouped', {}, alice); + const res = await api('/i/notifications', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); - assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); + assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); }); }); }); diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts index 87134794d2..21b45c41b8 100644 --- a/packages/backend/test/e2e/nodeinfo.ts +++ b/packages/backend/test/e2e/nodeinfo.ts @@ -1,14 +1,25 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { relativeFetch } from '../utils.js'; +import { relativeFetch, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('nodeinfo', () => { + let app: INestApplicationContext; + + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + test('nodeinfo 2.1', async () => { const res = await relativeFetch('nodeinfo/2.1'); assert.ok(res.ok); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index b1effa74bc..179bb80398 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -1,41 +1,42 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Repository } from "typeorm"; - process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { MiNote } from '@/models/Note.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; +import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('Note', () => { - let Notes: Repository; + let app: INestApplicationContext; + let Notes: any; - let root: misskey.entities.SignupResponse; - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let tom: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); const connection = await initTestDb(true); Notes = connection.getRepository(MiNote); - root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - tom = await signup({ username: 'tom', host: 'example.com' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('投稿できる', async () => { const post = { text: 'test', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -43,9 +44,9 @@ describe('Note', () => { }); test('ファイルを添付できる', async () => { - const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/192.jpg'); + const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/Lenna.jpg'); - const res = await api('notes/create', { + const res = await api('/notes/create', { fileIds: [file.id], }, alice); @@ -55,36 +56,36 @@ describe('Note', () => { }, 1000 * 10); test('他人のファイルで怒られる', async () => { - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/192.jpg'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/Lenna.jpg'); - const res = await api('notes/create', { + const res = await api('/notes/create', { text: 'test', fileIds: [file.id], }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); - assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }, 1000 * 10); test('存在しないファイルで怒られる', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { text: 'test', fileIds: ['000000000000000000000000'], }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); - assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }); test('不正なファイルIDで怒られる', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { fileIds: ['kyoppie'], }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); - assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }); test('返信できる', async () => { @@ -97,13 +98,12 @@ describe('Note', () => { replyId: bobPost.id, }; - const res = await api('notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.text, alicePost.text); assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); - assert.ok(res.body.createdNote.reply); assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); }); @@ -116,12 +116,11 @@ describe('Note', () => { renoteId: bobPost.id, }; - const res = await api('notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.ok(res.body.createdNote.renote); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); }); @@ -135,31 +134,17 @@ describe('Note', () => { renoteId: bobPost.id, }; - const res = await api('notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.text, alicePost.text); assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.ok(res.body.createdNote.renote); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); }); - test('引用renoteで空白文字のみで構成されたtextにするとレスポンスがtext: nullになる', async () => { - const bobPost = await post(bob, { - text: 'test', - }); - const res = await api('notes/create', { - text: ' ', - renoteId: bobPost.id, - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.createdNote.text, null); - }); - test('visibility: followersでrenoteできる', async () => { - const createRes = await api('notes/create', { + const createRes = await api('/notes/create', { text: 'test', visibility: 'followers', }, alice); @@ -167,7 +152,7 @@ describe('Note', () => { assert.strictEqual(createRes.status, 200); const renoteId = createRes.body.createdNote.id; - const renoteRes = await api('notes/create', { + const renoteRes = await api('/notes/create', { visibility: 'followers', renoteId, }, alice); @@ -176,99 +161,18 @@ describe('Note', () => { assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId); assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers'); - const deleteRes = await api('notes/delete', { + const deleteRes = await api('/notes/delete', { noteId: renoteRes.body.createdNote.id, }, alice); assert.strictEqual(deleteRes.status, 204); }); - test('visibility: followersなノートに対してフォロワーはリプライできる', async () => { - await api('following/create', { - userId: alice.id, - }, bob); - - const aliceNote = await api('notes/create', { - text: 'direct note to bob', - visibility: 'followers', - }, alice); - - assert.strictEqual(aliceNote.status, 200); - - const replyId = aliceNote.body.createdNote.id; - const bobReply = await api('notes/create', { - text: 'reply to alice note', - replyId, - }, bob); - - assert.strictEqual(bobReply.status, 200); - assert.strictEqual(bobReply.body.createdNote.replyId, replyId); - - await api('following/delete', { - userId: alice.id, - }, bob); - }); - - test('visibility: followersなノートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => { - const aliceNote = await api('notes/create', { - text: 'direct note to bob', - visibility: 'followers', - }, alice); - - assert.strictEqual(aliceNote.status, 200); - - const bobReply = await api('notes/create', { - text: 'reply to alice note', - replyId: aliceNote.body.createdNote.id, - }, bob); - - assert.strictEqual(bobReply.status, 400); - assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE'); - }); - - test('visibility: specifiedなノートに対してvisibility: specifiedで返信できる', async () => { - const aliceNote = await api('notes/create', { - text: 'direct note to bob', - visibility: 'specified', - visibleUserIds: [bob.id], - }, alice); - - assert.strictEqual(aliceNote.status, 200); - - const bobReply = await api('notes/create', { - text: 'reply to alice note', - replyId: aliceNote.body.createdNote.id, - visibility: 'specified', - visibleUserIds: [alice.id], - }, bob); - - assert.strictEqual(bobReply.status, 200); - }); - - test('visibility: specifiedなノートに対してvisibility: follwersで返信しようとすると怒られる', async () => { - const aliceNote = await api('notes/create', { - text: 'direct note to bob', - visibility: 'specified', - visibleUserIds: [bob.id], - }, alice); - - assert.strictEqual(aliceNote.status, 200); - - const bobReply = await api('notes/create', { - text: 'reply to alice note with visibility: followers', - replyId: aliceNote.body.createdNote.id, - visibility: 'followers', - }, bob); - - assert.strictEqual(bobReply.status, 400); - assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY'); - }); - test('文字数ぎりぎりで怒られない', async () => { const post = { text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字 }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); }); @@ -276,7 +180,7 @@ describe('Note', () => { const post = { text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字 }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -285,7 +189,7 @@ describe('Note', () => { text: 'test', replyId: '000000000000000000000000', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -293,7 +197,7 @@ describe('Note', () => { const post = { renoteId: '000000000000000000000000', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -302,7 +206,7 @@ describe('Note', () => { text: 'test', replyId: 'foo', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -310,7 +214,7 @@ describe('Note', () => { const post = { renoteId: 'foo', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -319,7 +223,7 @@ describe('Note', () => { text: '@ghost yo', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -331,139 +235,129 @@ describe('Note', () => { text: '@bob @bob @bob yo', }; - const res = await api('notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.text, post.text); const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); - assert.ok(noteDoc); assert.deepStrictEqual(noteDoc.mentions, [bob.id]); }); describe('添付ファイル情報', () => { test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const res = await api('notes/create', { - fileIds: [file.body!.id], + const res = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.ok(res.body.createdNote.files); assert.strictEqual(res.body.createdNote.files.length, 1); - assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id); + assert.strictEqual(res.body.createdNote.files[0].id, file.body.id); }); test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const createdNote = await api('notes/create', { - fileIds: [file.body!.id], + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(createdNote.status, 200); - const res = await api('notes', { + const res = await api('/notes', { withFiles: true, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - const myNote = res.body.find(note => note.id === createdNote.body.createdNote.id); - assert.ok(myNote); - assert.ok(myNote.files); + const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id); + assert.notEqual(myNote, null); assert.strictEqual(myNote.files.length, 1); - assert.strictEqual(myNote.files[0].id, file.body!.id); + assert.strictEqual(myNote.files[0].id, file.body.id); }); test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const createdNote = await api('notes/create', { - fileIds: [file.body!.id], + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(createdNote.status, 200); - const renoted = await api('notes/create', { + const renoted = await api('/notes/create', { renoteId: createdNote.body.createdNote.id, }, alice); assert.strictEqual(renoted.status, 200); - const res = await api('notes', { + const res = await api('/notes', { renote: true, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); - assert.ok(myNote); - assert.ok(myNote.renote); - assert.ok(myNote.renote.files); + assert.notEqual(myNote, null); assert.strictEqual(myNote.renote.files.length, 1); - assert.strictEqual(myNote.renote.files[0].id, file.body!.id); + assert.strictEqual(myNote.renote.files[0].id, file.body.id); }); test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const createdNote = await api('notes/create', { - fileIds: [file.body!.id], + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(createdNote.status, 200); - const reply = await api('notes/create', { + const reply = await api('/notes/create', { replyId: createdNote.body.createdNote.id, text: 'this is reply', }, alice); assert.strictEqual(reply.status, 200); - const res = await api('notes', { + const res = await api('/notes', { reply: true, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id); - assert.ok(myNote); - assert.ok(myNote.reply); - assert.ok(myNote.reply.files); + assert.notEqual(myNote, null); assert.strictEqual(myNote.reply.files.length, 1); - assert.strictEqual(myNote.reply.files[0].id, file.body!.id); + assert.strictEqual(myNote.reply.files[0].id, file.body.id); }); test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { const file = await uploadFile(alice); - const createdNote = await api('notes/create', { - fileIds: [file.body!.id], + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], }, alice); assert.strictEqual(createdNote.status, 200); - const reply = await api('notes/create', { + const reply = await api('/notes/create', { replyId: createdNote.body.createdNote.id, text: 'this is reply', }, alice); assert.strictEqual(reply.status, 200); - const renoted = await api('notes/create', { + const renoted = await api('/notes/create', { renoteId: reply.body.createdNote.id, }, alice); assert.strictEqual(renoted.status, 200); - const res = await api('notes', { + const res = await api('/notes', { renote: true, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); - assert.ok(myNote); - assert.ok(myNote.renote); - assert.ok(myNote.renote.reply); - assert.ok(myNote.renote.reply.files); + assert.notEqual(myNote, null); assert.strictEqual(myNote.renote.reply.files.length, 1); - assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id); + assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id); }); test('NSFWが強制されている場合変更できない', async () => { @@ -490,33 +384,33 @@ describe('Note', () => { value: true, }, }, - }, root); + }, alice); assert.strictEqual(res.status, 200); const assign = await api('admin/roles/assign', { userId: alice.id, roleId: res.body.id, - }, root); + }, alice); assert.strictEqual(assign.status, 204); - assert.strictEqual(file.body!.isSensitive, false); + assert.strictEqual(file.body.isSensitive, false); const nsfwfile = await uploadFile(alice); assert.strictEqual(nsfwfile.status, 200); - assert.strictEqual(nsfwfile.body!.isSensitive, true); + assert.strictEqual(nsfwfile.body.isSensitive, true); const liftnsfw = await api('drive/files/update', { - fileId: nsfwfile.body!.id, + fileId: nsfwfile.body.id, isSensitive: false, }, alice); assert.strictEqual(liftnsfw.status, 400); - assert.strictEqual(castAsError(liftnsfw.body).error.code, 'RESTRICTED_BY_ROLE'); + assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE'); const oldaddnsfw = await api('drive/files/update', { - fileId: file.body!.id, + fileId: file.body.id, isSensitive: true, }, alice); @@ -525,17 +419,17 @@ describe('Note', () => { await api('admin/roles/unassign', { userId: alice.id, roleId: res.body.id, - }, root); + }); await api('admin/roles/delete', { roleId: res.body.id, - }, root); + }, alice); }); }); describe('notes/create', () => { test('投票を添付できる', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { text: 'test', poll: { choices: ['foo', 'bar'], @@ -548,15 +442,14 @@ describe('Note', () => { }); test('投票の選択肢が無くて怒られる', async () => { - const res = await api('notes/create', { - // @ts-expect-error poll must not be empty + const res = await api('/notes/create', { poll: {}, }, alice); assert.strictEqual(res.status, 400); }); test('投票の選択肢が無くて怒られる (空の配列)', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { poll: { choices: [], }, @@ -565,7 +458,7 @@ describe('Note', () => { }); test('投票の選択肢が1つで怒られる', async () => { - const res = await api('notes/create', { + const res = await api('/notes/create', { poll: { choices: ['Strawberry Pasta'], }, @@ -574,14 +467,14 @@ describe('Note', () => { }); test('投票できる', async () => { - const { body } = await api('notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], }, }, alice); - const res = await api('notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); @@ -590,19 +483,19 @@ describe('Note', () => { }); test('複数投票できない', async () => { - const { body } = await api('notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], }, }, alice); - await api('notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 0, }, alice); - const res = await api('notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 2, }, alice); @@ -611,7 +504,7 @@ describe('Note', () => { }); test('許可されている場合は複数投票できる', async () => { - const { body } = await api('notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], @@ -619,17 +512,17 @@ describe('Note', () => { }, }, alice); - await api('notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 0, }, alice); - await api('notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); - const res = await api('notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 2, }, alice); @@ -638,7 +531,7 @@ describe('Note', () => { }); test('締め切られている場合は投票できない', async () => { - const { body } = await api('notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], @@ -648,7 +541,7 @@ describe('Note', () => { await new Promise(x => setTimeout(x, 2)); - const res = await api('notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); @@ -661,13 +554,13 @@ describe('Note', () => { sensitiveWords: [ 'test', ], - }, root); + }, alice); assert.strictEqual(sensitive.status, 204); await new Promise(x => setTimeout(x, 2)); - const note1 = await api('notes/create', { + const note1 = await api('/notes/create', { text: 'hogetesthuge', }, alice); @@ -680,11 +573,11 @@ describe('Note', () => { sensitiveWords: [ '/Test/i', ], - }, root); + }, alice); assert.strictEqual(sensitive.status, 204); - const note2 = await api('notes/create', { + const note2 = await api('/notes/create', { text: 'hogetesthuge', }, alice); @@ -697,253 +590,17 @@ describe('Note', () => { sensitiveWords: [ 'Test hoge', ], - }, root); + }, alice); assert.strictEqual(sensitive.status, 204); - const note2 = await api('notes/create', { + const note2 = await api('/notes/create', { text: 'hogeTesthuge', }, alice); assert.strictEqual(note2.status, 200); assert.strictEqual(note2.body.createdNote.visibility, 'home'); }); - - test('禁止ワードを含む投稿はエラーになる (単語指定)', async () => { - const prohibited = await api('admin/update-meta', { - prohibitedWords: [ - 'test', - ], - }, root); - - assert.strictEqual(prohibited.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note1 = await api('notes/create', { - text: 'hogetesthuge', - }, alice); - - assert.strictEqual(note1.status, 400); - assert.strictEqual(castAsError(note1.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); - }); - - test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => { - const prohibited = await api('admin/update-meta', { - prohibitedWords: [ - '/Test/i', - ], - }, root); - - assert.strictEqual(prohibited.status, 204); - - const note2 = await api('notes/create', { - text: 'hogetesthuge', - }, alice); - - assert.strictEqual(note2.status, 400); - assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); - }); - - test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => { - const prohibited = await api('admin/update-meta', { - prohibitedWords: [ - 'Test hoge', - ], - }, root); - - assert.strictEqual(prohibited.status, 204); - - const note2 = await api('notes/create', { - text: 'hogeTesthuge', - }, alice); - - assert.strictEqual(note2.status, 400); - assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); - }); - - test('禁止ワードを含んでるリモートノートもエラーになる', async () => { - const prohibited = await api('admin/update-meta', { - prohibitedWords: [ - 'test', - ], - }, root); - - assert.strictEqual(prohibited.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note1 = await api('notes/create', { - text: 'hogetesthuge', - }, tom); - - assert.strictEqual(note1.status, 400); - }); - - test('メンションの数が上限を超えるとエラーになる', async () => { - const res = await api('admin/roles/create', { - name: 'test', - description: '', - color: null, - iconUrl: null, - displayOrder: 0, - target: 'manual', - condFormula: {}, - isAdministrator: false, - isModerator: false, - isPublic: false, - isExplorable: false, - asBadge: false, - canEditMembersByModerator: false, - policies: { - mentionLimit: { - useDefault: false, - priority: 1, - value: 0, - }, - }, - }, root); - - assert.strictEqual(res.status, 200); - - await new Promise(x => setTimeout(x, 2)); - - const assign = await api('admin/roles/assign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - assert.strictEqual(assign.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note = await api('notes/create', { - text: '@bob potentially annoying text', - }, alice); - - assert.strictEqual(note.status, 400); - assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS'); - - await api('admin/roles/unassign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - await api('admin/roles/delete', { - roleId: res.body.id, - }, root); - }); - - test('ダイレクト投稿もエラーになる', async () => { - const res = await api('admin/roles/create', { - name: 'test', - description: '', - color: null, - iconUrl: null, - displayOrder: 0, - target: 'manual', - condFormula: {}, - isAdministrator: false, - isModerator: false, - isPublic: false, - isExplorable: false, - asBadge: false, - canEditMembersByModerator: false, - policies: { - mentionLimit: { - useDefault: false, - priority: 1, - value: 0, - }, - }, - }, root); - - assert.strictEqual(res.status, 200); - - await new Promise(x => setTimeout(x, 2)); - - const assign = await api('admin/roles/assign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - assert.strictEqual(assign.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note = await api('notes/create', { - text: 'potentially annoying text', - visibility: 'specified', - visibleUserIds: [bob.id], - }, alice); - - assert.strictEqual(note.status, 400); - assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS'); - - await api('admin/roles/unassign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - await api('admin/roles/delete', { - roleId: res.body.id, - }, root); - }); - - test('ダイレクトの宛先とメンションが同じ場合は重複してカウントしない', async () => { - const res = await api('admin/roles/create', { - name: 'test', - description: '', - color: null, - iconUrl: null, - displayOrder: 0, - target: 'manual', - condFormula: {}, - isAdministrator: false, - isModerator: false, - isPublic: false, - isExplorable: false, - asBadge: false, - canEditMembersByModerator: false, - policies: { - mentionLimit: { - useDefault: false, - priority: 1, - value: 1, - }, - }, - }, root); - - assert.strictEqual(res.status, 200); - - await new Promise(x => setTimeout(x, 2)); - - const assign = await api('admin/roles/assign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - assert.strictEqual(assign.status, 204); - - await new Promise(x => setTimeout(x, 2)); - - const note = await api('notes/create', { - text: '@bob potentially annoying text', - visibility: 'specified', - visibleUserIds: [bob.id], - }, alice); - - assert.strictEqual(note.status, 200); - - await api('admin/roles/unassign', { - userId: alice.id, - roleId: res.body.id, - }, root); - - await api('admin/roles/delete', { - roleId: res.body.id, - }, root); - }); }); describe('notes/delete', () => { @@ -966,7 +623,6 @@ describe('Note', () => { assert.strictEqual(deleteOneRes.status, 204); let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.ok(mainNote); assert.strictEqual(mainNote.repliesCount, 1); const deleteTwoRes = await api('notes/delete', { @@ -975,65 +631,7 @@ describe('Note', () => { assert.strictEqual(deleteTwoRes.status, 204); mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.ok(mainNote); assert.strictEqual(mainNote.repliesCount, 0); }); }); - - describe('notes/translate', () => { - describe('翻訳機能の利用が許可されていない場合', () => { - let cannotTranslateRole: misskey.entities.Role; - - beforeAll(async () => { - cannotTranslateRole = await role(root, {}, { canUseTranslator: false }); - await api('admin/roles/assign', { roleId: cannotTranslateRole.id, userId: alice.id }, root); - }); - - test('翻訳機能の利用が許可されていない場合翻訳できない', async () => { - const aliceNote = await post(alice, { text: 'Hello' }); - const res = await api('notes/translate', { - noteId: aliceNote.id, - targetLang: 'ja', - }, alice); - - assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE'); - }); - - afterAll(async () => { - await api('admin/roles/unassign', { roleId: cannotTranslateRole.id, userId: alice.id }, root); - }); - }); - - test('存在しないノートは翻訳できない', async () => { - const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice); - - assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_NOTE'); - }); - - test('不可視なノートは翻訳できない', async () => { - const aliceNote = await post(alice, { visibility: 'followers', text: 'Hello' }); - const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob); - - assert.strictEqual(bobTranslateAttempt.status, 400); - assert.strictEqual(castAsError(bobTranslateAttempt.body).error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE'); - }); - - test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => { - const aliceNote = await post(alice, { text: null, poll: { choices: ['kinoko', 'takenoko'] } }); - const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice); - - assert.strictEqual(res.status, 204); - }); - - test('サーバーに DeepL 認証キーが登録されていない場合翻訳できない', async () => { - const aliceNote = await post(alice, { text: 'Hello' }); - const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice); - - // NOTE: デフォルトでは登録されていないので落ちる - assert.strictEqual(res.status, 400); - assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE'); - }); - }); }); diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index 7a47e17d99..38686d5582 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -11,18 +11,13 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { - AuthorizationCode, - type AuthorizationTokenConfig, - ClientCredentials, - ModuleOptions, - ResourceOwnerPassword, -} from 'simple-oauth2'; +import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials, ModuleOptions } from 'simple-oauth2'; import pkceChallenge from 'pkce-challenge'; import { JSDOM } from 'jsdom'; -import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify'; -import { api, port, sendEnvUpdateRequest, signup } from '../utils.js'; +import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify'; +import { api, port, signup, startServer } from '../utils.js'; import type * as misskey from 'cherrypick-js'; +import type { INestApplicationContext } from '@nestjs/common'; const host = `http://127.0.0.1:${port}`; @@ -80,7 +75,7 @@ function getMeta(html: string): { transactionId: string | undefined, clientName: }; } -function fetchDecision(transactionId: string, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise { +function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { return fetch(new URL('/oauth/decision', host), { method: 'post', body: new URLSearchParams({ @@ -95,14 +90,14 @@ function fetchDecision(transactionId: string, user: misskey.entities.SignupRespo }); } -async function fetchDecisionFromResponse(response: Response, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise { +async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { const { transactionId } = getMeta(await response.text()); assert.ok(transactionId); return await fetchDecision(transactionId, user, { cancel }); } -async function fetchAuthorizationCode(user: misskey.entities.SignupResponse, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { +async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { const client = new AuthorizationCode(clientConfig); const response = await fetch(client.authorizeURL({ @@ -152,14 +147,16 @@ async function assertDirectError(response: Response, status: number, error: stri } describe('OAuth', () => { + let app: INestApplicationContext; let fastify: FastifyInstance; - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; let sender: (reply: FastifyReply) => void; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); @@ -171,7 +168,7 @@ describe('OAuth', () => { }, 1000 * 60 * 2); beforeEach(async () => { - await sendEnvUpdateRequest({ key: 'CHERRYPICK_TEST_CHECK_IP_RANGE', value: '' }); + process.env.CHERRYPICK_TEST_CHECK_IP_RANGE = ''; sender = (reply): void => { reply.send(` @@ -183,6 +180,7 @@ describe('OAuth', () => { afterAll(async () => { await fastify.close(); + await app.close(); }); test('Full flow', async () => { @@ -216,7 +214,7 @@ describe('OAuth', () => { assert.ok(location.searchParams.has('code')); assert.strictEqual(location.searchParams.get('state'), 'state'); // https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss - assert.strictEqual(location.searchParams.get('iss'), 'http://cherrypick.local'); + assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local'); const code = new URL(location).searchParams.get('code'); assert.ok(code); @@ -605,7 +603,7 @@ describe('OAuth', () => { bearer: true, }); assert.strictEqual(createResult.status, 403); - assert.ok(createResult.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="CherryPick", error="insufficient_scope", error_description')); + assert.ok(createResult.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="insufficient_scope", error_description')); }); }); @@ -704,7 +702,7 @@ describe('OAuth', () => { assert.strictEqual(response.status, 200); const body = await response.json(); - assert.strictEqual(body.issuer, 'http://cherrypick.local'); + assert.strictEqual(body.issuer, 'http://misskey.local'); assert.ok(body.scopes_supported.includes('write:notes')); }); @@ -883,7 +881,7 @@ describe('OAuth', () => { }); test('Disallow loopback', async () => { - await sendEnvUpdateRequest({ key: 'CHERRYPICK_TEST_CHECK_IP_RANGE', value: '1' }); + process.env.CHERRYPICK_TEST_CHECK_IP_RANGE = '1'; const client = new AuthorizationCode(clientConfig); const response = await fetch(client.authorizeURL({ diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index 64f418dae2..b1e7a745ec 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -1,29 +1,36 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { setTimeout } from 'node:timers/promises'; -import { api, post, signup, waitFire } from '../utils.js'; +import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('Renote Mute', () => { + let app: INestApplicationContext; + // alice mutes carol - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('ミュート作成', async () => { - const res = await api('renote-mute/create', { + const res = await api('/renote-mute/create', { userId: carol.id, }, alice); @@ -36,15 +43,15 @@ describe('Renote Mute', () => { const carolNote = await post(carol, { text: 'hi' }); // redisに追加されるのを待つ - await setTimeout(100); + await sleep(100); - const res = await api('notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => { @@ -53,31 +60,15 @@ describe('Renote Mute', () => { const carolNote = await post(carol, { text: 'hi' }); // redisに追加されるのを待つ - await setTimeout(100); - - const res = await api('notes/local-timeline', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - }); - - // #12956 - test('タイムラインにリノートミュートしているユーザーの通常ノートのリノートが含まれる', async () => { - const carolNote = await post(carol, { text: 'hi' }); - const bobRenote = await post(bob, { renoteId: carolNote.id }); - - // redisに追加されるのを待つ - await setTimeout(100); + await sleep(100); - const res = await api('notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => { @@ -103,17 +94,4 @@ describe('Renote Mute', () => { assert.strictEqual(fired, true); }); - - // #12956 - test('ストリームにリノートミュートしているユーザーの通常ノートのリノートが流れてくる', async () => { - const carolbNote = await post(carol, { text: 'hi' }); - - const fired = await waitFire( - alice, 'localTimeline', - () => api('notes/create', { renoteId: carolbNote.id }, bob), - msg => msg.type === 'note' && msg.body.userId === bob.id, - ); - - assert.strictEqual(fired, true); - }); }); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 76bf10cf66..9d7d9a3074 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,10 +8,12 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { WebSocket } from 'ws'; import { MiFollowing } from '@/models/Following.js'; -import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js'; +import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('Streaming', () => { + let app: INestApplicationContext; let Followings: any; const follow = async (follower: any, followee: any) => { @@ -30,23 +32,23 @@ describe('Streaming', () => { describe('Streaming', () => { // Local users - let ayano: misskey.entities.SignupResponse; - let kyoko: misskey.entities.SignupResponse; - let chitose: misskey.entities.SignupResponse; - let kanako: misskey.entities.SignupResponse; - let erin: misskey.entities.SignupResponse; + let ayano: misskey.entities.MeSignup; + let kyoko: misskey.entities.MeSignup; + let chitose: misskey.entities.MeSignup; + let kanako: misskey.entities.MeSignup; // Remote users - let akari: misskey.entities.SignupResponse; - let chinatsu: misskey.entities.SignupResponse; - let takumi: misskey.entities.SignupResponse; + let akari: misskey.entities.MeSignup; + let chinatsu: misskey.entities.MeSignup; + let takumi: misskey.entities.MeSignup; - let kyokoNote: misskey.entities.Note; - let kanakoNote: misskey.entities.Note; - let takumiNote: misskey.entities.Note; + let kyokoNote: any; + let kanakoNote: any; + let takumiNote: any; let list: any; beforeAll(async () => { + app = await startServer(); const connection = await initTestDb(true); Followings = connection.getRepository(MiFollowing); @@ -54,7 +56,6 @@ describe('Streaming', () => { kyoko = await signup({ username: 'kyoko' }); chitose = await signup({ username: 'chitose' }); kanako = await signup({ username: 'kanako' }); - erin = await signup({ username: 'erin' }); // erin: A generic fifth participant akari = await signup({ username: 'akari', host: 'example.com' }); chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); @@ -65,20 +66,11 @@ describe('Streaming', () => { takumiNote = await post(takumi, { text: 'piyo' }); // Follow: ayano => kyoko - await api('following/create', { userId: kyoko.id, withReplies: false }, ayano); + await api('following/create', { userId: kyoko.id }, ayano); // Follow: ayano => akari await follow(ayano, akari); - // Follow: kyoko => chitose - await api('following/create', { userId: chitose.id }, kyoko); - - // Follow: erin <=> ayano each other. - // erin => ayano: withReplies: true - await api('following/create', { userId: ayano.id, withReplies: true }, erin); - // ayano => erin: withReplies: false - await api('following/create', { userId: erin.id, withReplies: false }, ayano); - // Mute: chitose => kanako await api('mute/create', { userId: kanako.id }, chitose); @@ -103,6 +95,10 @@ describe('Streaming', () => { }, chitose); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + describe('Events', () => { test('mention event', async () => { const fired = await waitFire( @@ -166,41 +162,22 @@ describe('Streaming', () => { assert.strictEqual(fired, true); }); + /* なんか失敗する test('フォローしているユーザーの visibility: followers な投稿への返信が流れる', async () => { - const note = await post(kyoko, { text: 'foo', visibility: 'followers' }); + const note = await api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko); const fired = await waitFire( ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.id }, kyoko), // kyoko posts + () => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.body.id }, kyoko), // kyoko posts msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo', ); assert.strictEqual(fired, true); }); + */ test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => { - const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'reply to chitose\'s followers-only post', replyId: chitoseNote.id }, kyoko), // kyoko's reply to chitose's followers-only post - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信のリノートが流れない', async () => { - const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); - const kyokoReply = await post(kyoko, { text: 'reply to followers-only post', replyId: chitoseNote.id }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { renoteId: kyokoReply.id }, kyoko), // kyoko's renote of kyoko's reply to chitose's followers-only post - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); + // TODO }); test('フォローしていないユーザーの投稿は流れない', async () => { @@ -232,101 +209,6 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); - - /** - * TODO: 落ちる - * @see https://github.com/misskey-dev/misskey/issues/13474 - test('visibility: specified なノートで visibleUserIds に自分が含まれているときそのノートへのリプライが流れてくる', async () => { - const chitoseToKyokoAndAyano = await post(chitose, { text: 'direct note from chitose to kyoko and ayano', visibility: 'specified', visibleUserIds: [kyoko.id, ayano.id] }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'direct reply from kyoko to chitose and ayano', replyId: chitoseToKyokoAndAyano.id, visibility: 'specified', visibleUserIds: [chitose.id, ayano.id] }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - ); - - assert.strictEqual(fired, true); - }); - */ - - test('visibility: specified な投稿に対するリプライで visibleUserIds が拡張されたとき、その拡張されたユーザーの HTL にはそのリプライが流れない', async () => { - const chitoseToKyoko = await post(chitose, { text: 'direct note from chitose to kyoko', visibility: 'specified', visibleUserIds: [kyoko.id] }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'direct reply from kyoko to chitose and ayano', replyId: chitoseToKyoko.id, visibility: 'specified', visibleUserIds: [chitose.id, ayano.id] }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - ); - - assert.strictEqual(fired, false); - }); - - test('visibility: specified な投稿に対するリプライで visibleUserIds が収縮されたとき、その収縮されたユーザーの HTL にはそのリプライが流れない', async () => { - const chitoseToKyokoAndAyano = await post(chitose, { text: 'direct note from chitose to kyoko and ayano', visibility: 'specified', visibleUserIds: [kyoko.id, ayano.id] }); - - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'direct reply from kyoko to chitose', replyId: chitoseToKyokoAndAyano.id, visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - ); - - assert.strictEqual(fired, false); - }); - - test('withRenotes: false のときリノートが流れない', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { renoteId: kyokoNote.id }, kyoko), // kyoko renote - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - { withRenotes: false }, - ); - - assert.strictEqual(fired, false); - }); - - test('withRenotes: false のとき引用リノートが流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'quote', renoteId: kyokoNote.id }, kyoko), // kyoko quote - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - { withRenotes: false }, - ); - - assert.strictEqual(fired, true); - }); - - test('withRenotes: false のとき投票のみのリノートが流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { poll: { choices: ['kinoko', 'takenoko'] }, renoteId: kyokoNote.id }, kyoko), // kyoko renote with poll - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - { withRenotes: false }, - ); - - assert.strictEqual(fired, true); - }); - - test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => { - const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); - const fired = await waitFire( - erin, 'homeTimeline', // erin:home - () => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post - msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, true); - }); - - test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => { - const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post - msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin - ); - - assert.strictEqual(fired, true); - }); }); // Home describe('Local Timeline', () => { @@ -505,38 +387,6 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); - - test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => { - const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); - const fired = await waitFire( - erin, 'homeTimeline', // erin:home - () => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post - msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, true); - }); - - test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => { - const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post - msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin - ); - - assert.strictEqual(fired, true); - }); - - test('withReplies: true のフォローしていない人のfollowersノートに対するリプライが流れない', async () => { - const fired = await waitFire( - erin, 'homeTimeline', // erin:home - () => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano), // ayano reply to chitose's post - msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, false); - }); }); describe('Global Timeline', () => { @@ -571,16 +421,6 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); - - test('withReplies = falseでフォローしてる人によるリプライが流れてくる', async () => { - const fired = await waitFire( - ayano, 'globalTimeline', // ayano:Global - () => api('notes/create', { text: 'foo', replyId: kanakoNote.id }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); }); describe('UserList Timeline', () => { @@ -671,7 +511,7 @@ describe('Streaming', () => { // #10443 test('ミュートしているサーバのノートがリストTLに流れない', async () => { - await api('i/update', { + await api('/i/update', { mutedInstances: ['example.com'], }, chitose); @@ -688,7 +528,7 @@ describe('Streaming', () => { // #10443 test('ミュートしているサーバのノートに対するリプライがリストTLに流れない', async () => { - await api('i/update', { + await api('/i/update', { mutedInstances: ['example.com'], }, chitose); @@ -705,7 +545,7 @@ describe('Streaming', () => { // #10443 test('ミュートしているサーバのノートに対するリノートがリストTLに流れない', async () => { - await api('i/update', { + await api('/i/update', { mutedInstances: ['example.com'], }, chitose); diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts deleted file mode 100644 index 5bf20cfc89..0000000000 --- a/packages/backend/test/e2e/synalio/abuse-report.ts +++ /dev/null @@ -1,360 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { entities } from 'cherrypick-js'; -import { beforeEach, describe, test } from '@jest/globals'; -import { - api, - captureWebhook, - randomString, - role, - signup, - startJobQueue, - UserToken, - WEBHOOK_HOST, -} from '../../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; - -describe('[シナリオ] ユーザ通報', () => { - let queue: INestApplicationContext; - let admin: entities.SignupResponse; - let alice: entities.SignupResponse; - let bob: entities.SignupResponse; - - async function createSystemWebhook(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'admin/system-webhook/create', - { - isActive: true, - name: randomString(), - on: ['abuseReport'], - url: WEBHOOK_HOST, - secret: randomString(), - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - async function createAbuseReportNotificationRecipient(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'admin/abuse-report/notification-recipient/create', - { - isActive: true, - name: randomString(), - method: 'webhook', - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - async function createAbuseReport(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'users/report-abuse', - { - userId: alice.id, - comment: randomString(), - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - async function resolveAbuseReport(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'admin/resolve-abuse-user-report', - { - reportId: admin.id, - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - // ------------------------------------------------------------------------------------------- - - beforeAll(async () => { - queue = await startJobQueue(); - admin = await signup({ username: 'admin' }); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - - await role(admin, { isAdministrator: true }); - }, 1000 * 60 * 2); - - afterAll(async () => { - await queue.close(); - }); - - // ------------------------------------------------------------------------------------------- - - describe('SystemWebhook', () => { - beforeEach(async () => { - const webhooks = await api('admin/system-webhook/list', {}, admin); - for (const webhook of webhooks.body) { - await api('admin/system-webhook/delete', { id: webhook.id }, admin); - } - }); - - test('通報を受けた -> abuseReportが送出される', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }); - - console.log(JSON.stringify(webhookBody, null, 2)); - - expect(webhookBody.hookId).toBe(webhook.id); - expect(webhookBody.type).toBe('abuseReport'); - expect(webhookBody.body.targetUserId).toBe(alice.id); - expect(webhookBody.body.reporterId).toBe(bob.id); - expect(webhookBody.body.comment).toBe(abuse.comment); - }); - - test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが送出される', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport', 'abuseReportResolved'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }); - - console.log(JSON.stringify(webhookBody1, null, 2)); - expect(webhookBody1.hookId).toBe(webhook.id); - expect(webhookBody1.type).toBe('abuseReport'); - expect(webhookBody1.body.targetUserId).toBe(alice.id); - expect(webhookBody1.body.reporterId).toBe(bob.id); - expect(webhookBody1.body.assigneeId).toBeNull(); - expect(webhookBody1.body.resolved).toBe(false); - expect(webhookBody1.body.comment).toBe(abuse.comment); - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: webhookBody1.body.id, - forward: false, - }, admin); - }); - - console.log(JSON.stringify(webhookBody2, null, 2)); - expect(webhookBody2.hookId).toBe(webhook.id); - expect(webhookBody2.type).toBe('abuseReportResolved'); - expect(webhookBody2.body.targetUserId).toBe(alice.id); - expect(webhookBody2.body.reporterId).toBe(bob.id); - expect(webhookBody2.body.assigneeId).toBe(admin.id); - expect(webhookBody2.body.resolved).toBe(true); - expect(webhookBody2.body.comment).toBe(abuse.comment); - }); - - test('通報を受けた -> abuseReportが未許可の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: [], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody).toBe('timeout'); - }); - - test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが送出される', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReportResolved'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody1).toBe('timeout'); - - const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: abuseReportId, - forward: false, - }, admin); - }); - - console.log(JSON.stringify(webhookBody2, null, 2)); - expect(webhookBody2.hookId).toBe(webhook.id); - expect(webhookBody2.type).toBe('abuseReportResolved'); - expect(webhookBody2.body.targetUserId).toBe(alice.id); - expect(webhookBody2.body.reporterId).toBe(bob.id); - expect(webhookBody2.body.assigneeId).toBe(admin.id); - expect(webhookBody2.body.resolved).toBe(true); - expect(webhookBody2.body.comment).toBe(abuse.comment); - }); - - test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }); - - console.log(JSON.stringify(webhookBody1, null, 2)); - expect(webhookBody1.hookId).toBe(webhook.id); - expect(webhookBody1.type).toBe('abuseReport'); - expect(webhookBody1.body.targetUserId).toBe(alice.id); - expect(webhookBody1.body.reporterId).toBe(bob.id); - expect(webhookBody1.body.assigneeId).toBeNull(); - expect(webhookBody1.body.resolved).toBe(false); - expect(webhookBody1.body.comment).toBe(abuse.comment); - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: webhookBody1.body.id, - forward: false, - }, admin); - }).catch(e => e.message); - - expect(webhookBody2).toBe('timeout'); - }); - - test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: [], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody1).toBe('timeout'); - - const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: abuseReportId, - forward: false, - }, admin); - }).catch(e => e.message); - - expect(webhookBody2).toBe('timeout'); - }); - - test('通報を受けた -> Webhookが無効の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport', 'abuseReportResolved'], - isActive: false, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody1).toBe('timeout'); - - const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: abuseReportId, - forward: false, - }, admin); - }).catch(e => e.message); - - expect(webhookBody2).toBe('timeout'); - }); - - test('通報を受けた -> 通知設定が無効の場合は送出されない', async () => { - const webhook = await createSystemWebhook({ - on: ['abuseReport', 'abuseReportResolved'], - isActive: true, - }); - await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id, isActive: false }); - - // 通報(bob -> alice) - const abuse = { - userId: alice.id, - comment: randomString(), - }; - const webhookBody1 = await captureWebhook(async () => { - await createAbuseReport(abuse, bob); - }).catch(e => e.message); - - expect(webhookBody1).toBe('timeout'); - - const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; - - // 解決 - const webhookBody2 = await captureWebhook(async () => { - await resolveAbuseReport({ - reportId: abuseReportId, - forward: false, - }, admin); - }).catch(e => e.message); - - expect(webhookBody2).toBe('timeout'); - }); - }); -}); diff --git a/packages/backend/test/e2e/synalio/user-create.ts b/packages/backend/test/e2e/synalio/user-create.ts deleted file mode 100644 index d8c7235f19..0000000000 --- a/packages/backend/test/e2e/synalio/user-create.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { setTimeout } from 'node:timers/promises'; -import { entities } from 'cherrypick-js'; -import { beforeEach, describe, test } from '@jest/globals'; -import { - api, - captureWebhook, - randomString, - role, - signup, - startJobQueue, - UserToken, - WEBHOOK_HOST, -} from '../../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; - -describe('[シナリオ] ユーザ作成', () => { - let queue: INestApplicationContext; - let admin: entities.SignupResponse; - - async function createSystemWebhook(args?: Partial, credential?: UserToken): Promise { - const res = await api( - 'admin/system-webhook/create', - { - isActive: true, - name: randomString(), - on: ['userCreated'], - url: WEBHOOK_HOST, - secret: randomString(), - ...args, - }, - credential ?? admin, - ); - return res.body; - } - - // ------------------------------------------------------------------------------------------- - - beforeAll(async () => { - queue = await startJobQueue(); - admin = await signup({ username: 'admin' }); - - await role(admin, { isAdministrator: true }); - }, 1000 * 60 * 2); - - afterAll(async () => { - await queue.close(); - }); - - // ------------------------------------------------------------------------------------------- - - describe('SystemWebhook', () => { - beforeEach(async () => { - const webhooks = await api('admin/system-webhook/list', {}, admin); - for (const webhook of webhooks.body) { - await api('admin/system-webhook/delete', { id: webhook.id }, admin); - } - }); - - test('ユーザが作成された -> userCreatedが送出される', async () => { - const webhook = await createSystemWebhook({ - on: ['userCreated'], - isActive: true, - }); - - let alice: any = null; - const webhookBody = await captureWebhook(async () => { - alice = await signup({ username: 'alice' }); - }); - - // webhookの送出後にいろいろやってるのでちょっと待つ必要がある - await setTimeout(2000); - - console.log(alice); - console.log(JSON.stringify(webhookBody, null, 2)); - - expect(webhookBody.hookId).toBe(webhook.id); - expect(webhookBody.type).toBe('userCreated'); - - const body = webhookBody.body as entities.UserLite; - expect(alice.id).toBe(body.id); - expect(alice.name).toBe(body.name); - expect(alice.username).toBe(body.username); - expect(alice.host).toBe(body.host); - expect(alice.avatarUrl).toBe(body.avatarUrl); - expect(alice.avatarBlurhash).toBe(body.avatarBlurhash); - expect(alice.avatarDecorations).toEqual(body.avatarDecorations); - expect(alice.isBot).toBe(body.isBot); - expect(alice.isCat).toBe(body.isCat); - expect(alice.instance).toEqual(body.instance); - expect(alice.emojis).toEqual(body.emojis); - expect(alice.onlineStatus).toBe(body.onlineStatus); - expect(alice.badgeRoles).toEqual(body.badgeRoles); - }); - - test('ユーザ作成 -> userCreatedが未許可の場合は送出されない', async () => { - await createSystemWebhook({ - on: [], - isActive: true, - }); - - let alice: any = null; - const webhookBody = await captureWebhook(async () => { - alice = await signup({ username: 'alice' }); - }).catch(e => e.message); - - expect(webhookBody).toBe('timeout'); - expect(alice.id).not.toBeNull(); - }); - - test('ユーザ作成 -> Webhookが無効の場合は送出されない', async () => { - await createSystemWebhook({ - on: ['userCreated'], - isActive: false, - }); - - let alice: any = null; - const webhookBody = await captureWebhook(async () => { - alice = await signup({ username: 'alice' }); - }).catch(e => e.message); - - expect(webhookBody).toBe('timeout'); - expect(alice.id).not.toBeNull(); - }); - }); -}); diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 5e482f363c..9f50055259 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -1,54 +1,62 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, connectStream, post, signup } from '../utils.js'; +import { signup, api, post, connectStream, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('Note thread mute', () => { - let alice: misskey.entities.SignupResponse; - let bob: misskey.entities.SignupResponse; - let carol: misskey.entities.SignupResponse; + let app: INestApplicationContext; + + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => { const bobNote = await post(bob, { text: '@alice @carol root note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); - await api('notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - const res = await api('notes/mentions', {}, alice); + const res = await api('/notes/mentions', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolReply.id), false); - assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); }); test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { // 状態リセット - await api('i/read-all-unread-notes', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); const bobNote = await post(bob, { text: '@alice @carol root note' }); - await api('notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - const res = await api('i', {}, alice); + const res = await api('/i', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(res.body.hasUnreadMentions, false); @@ -56,11 +64,11 @@ describe('Note thread mute', () => { test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { // 状態リセット - await api('i/read-all-unread-notes', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); const bobNote = await post(bob, { text: '@alice @carol root note' }); - await api('notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); let fired = false; @@ -84,17 +92,17 @@ describe('Note thread mute', () => { const bobNote = await post(bob, { text: '@alice @carol root note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); - await api('notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - const res = await api('i/notifications', {}, alice); + const res = await api('/i/notifications', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReply.id), false); - assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReplyWithoutMention.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい }); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index d12be2a9ac..ed125da6d3 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -1,32 +1,37 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ // How to run: // pnpm jest -- e2e/timelines.ts +process.env.NODE_ENV = 'test'; +process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true'; + import * as assert from 'assert'; -import { setTimeout } from 'node:timers/promises'; -import { Redis } from 'ioredis'; -import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js'; -import { loadConfig } from '@/config.js'; +import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; function genHost() { return randomString() + '.example.com'; } function waitForPushToTl() { - return setTimeout(500); + return sleep(500); } -let redisForTimelines: Redis; +let app: INestApplicationContext; -describe('Timelines', () => { - beforeAll(() => { - redisForTimelines = new Redis(loadConfig().redisForTimelines); - }); +beforeAll(async () => { + app = await startServer(); +}, 1000 * 60 * 2); +afterAll(async () => { + await app.close(); +}); + +describe('Timelines', () => { describe('Home TL', () => { test.concurrent('自分の visibility: followers なノートが含まれる', async () => { const [alice] = await Promise.all([signup()]); @@ -35,199 +40,180 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); }); test.concurrent('フォローしているユーザーのノートが含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const carolNote = await post(carol, { text: 'hi' }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: carol.id }, bob); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/create', { userId: carol.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/following/create', { userId: carol.id }, alice); + await api('/following/create', { userId: carol.id }, bob); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi'); }); test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/following/create', { userId: carol.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }); test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('自分の他人への返信が含まれる', async () => { @@ -238,148 +224,146 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); }); test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { + const res = await api('/notes/timeline', { withRenotes: false, }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { + const res = await api('/notes/timeline', { withRenotes: false, }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); - + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); - + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const [bobFile, carolFile] = await Promise.all([ uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), @@ -391,27 +375,27 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice); + const res = await api('/notes/timeline', { limit: 100, withFiles: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - 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); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false); }, 1000 * 10); test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('自分の visibility: specified なノートが含まれる', async () => { @@ -421,25 +405,25 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); }); test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); }); test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => { @@ -449,23 +433,23 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => { @@ -476,10 +460,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok'); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok'); }); /* TODO @@ -491,10 +475,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok'); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'ok'); }); */ @@ -507,47 +491,9 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { - userId: alice.id, - }, bob); - - const aliceNote = await post(alice, { text: 'I\'m Alice.' }); - const bobNote = await post(bob, { text: 'I\'m Bob.' }); - const carolNote = await post(carol, { text: 'I\'m Carol.' }); - - await waitForPushToTl(); - - // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1); - - const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1); - assert.strictEqual(bobHTL.includes(aliceNote.id), true); - assert.strictEqual(bobHTL.includes(bobNote.id), true); - assert.strictEqual(bobHTL.includes(carolNote.id), false); - }); - - test.concurrent('FTT: リモートユーザーの HTL にはプッシュされない', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - - await api('following/create', { - userId: alice.id, - }, bob); - - await post(alice, { text: 'I\'m Alice.' }); - await post(bob, { text: 'I\'m Bob.' }); - - await waitForPushToTl(); + const res = await api('/notes/timeline', { limit: 100 }, alice); - // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); }); @@ -560,10 +506,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('他人の他人への返信が含まれない', async () => { @@ -574,10 +520,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); test.concurrent('他人のその人自身への返信が含まれる', async () => { @@ -588,23 +534,23 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }); test.concurrent('チャンネル投稿が含まれない', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('リモートユーザーのノートが含まれない', async () => { @@ -614,108 +560,93 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); // 含まれても良いと思うけど実装が面倒なので含まれない test.concurrent('フォローしているユーザーの visibility: home なノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: carol.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: carol.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('ミュートしているユーザーのノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); }); test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { @@ -726,9 +657,9 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice); + const res = await api('/notes/local-timeline', { limit: 100, withReplies: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { @@ -740,10 +671,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice); + const res = await api('/notes/local-timeline', { limit: 100, withFiles: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }, 1000 * 10); }); @@ -755,9 +686,9 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => { @@ -767,95 +698,39 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: carol.id }, bob); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/create', { userId: carol.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); - }); - - test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('他人の他人への返信が含まれない', async () => { @@ -866,10 +741,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); }); test.concurrent('リモートユーザーのノートが含まれない', async () => { @@ -879,54 +754,37 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('/notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); - + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); - + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); - - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { @@ -937,9 +795,9 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { @@ -951,10 +809,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); + const res = await api('/notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }, 1000 * 10); }); @@ -962,244 +820,226 @@ describe('Timelines', () => { test.concurrent('リスインしているフォローしていないユーザーのノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }); test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); - - test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - - await waitForPushToTl(); - - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id, withReplies: false }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('/users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); }); test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => { const [alice] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: alice.id }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: alice.id }, alice); + await sleep(1000); const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); }); test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }, 1000 * 10); test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); }); test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); - await setTimeout(1000); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('/users/lists/push', { listId: list.id, userId: carol.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); }); @@ -1211,9 +1051,9 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('/users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => { @@ -1223,24 +1063,24 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('/users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('/users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); }); test.concurrent('自身の visibility: followers なノートが含まれる', async () => { @@ -1250,23 +1090,23 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: alice.id }, alice); + const res = await api('/users/notes', { userId: alice.id }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); }); test.concurrent('チャンネル投稿が含まれない', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('/users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => { @@ -1278,10 +1118,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('/users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); }); test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => { @@ -1293,10 +1133,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); + const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }); test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => { @@ -1308,10 +1148,10 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); + const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); }); test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { @@ -1323,82 +1163,82 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice); + const res = await api('/users/notes', { userId: bob.id, withFiles: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }, 1000 * 10); test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); + const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); + const channel = await api('/channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); + const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => { const [bob] = await Promise.all([signup()]); - const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); + const channel = await api('/channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob); + const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, bob); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('/users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await api('mute/create', { userId: bob.id }, alice); - await setTimeout(1000); + await api('/mute/create', { userId: bob.id }, alice); + await sleep(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('/users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true); }); test.concurrent('自身の visibility: specified なノートが含まれる', async () => { @@ -1408,9 +1248,9 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice); + const res = await api('/users/notes', { userId: alice.id, withReplies: true }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); }); test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => { @@ -1420,36 +1260,9 @@ describe('Timelines', () => { await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); - - /** @see https://github.com/misskey-dev/misskey/issues/14000 */ - test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId による絞り込みが正しく動作する', async () => { - const alice = await signup(); - const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); - const note1 = await post(alice, { text: '1' }); - const note2 = await post(alice, { text: '2' }); - await redisForTimelines.del('list:userTimeline:' + alice.id); - const note3 = await post(alice, { text: '3' }); - - const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id }); - assert.deepStrictEqual(res.body, [note1, note2, note3]); - }); - - test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => { - const alice = await signup(); - const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); - const note1 = await post(alice, { text: '1' }); - const note2 = await post(alice, { text: '2' }); - await redisForTimelines.del('list:userTimeline:' + alice.id); - const note3 = await post(alice, { text: '3' }); - const noteUntil = await post(alice, { text: 'Note where id will be `untilId`.' }); - await post(alice, { text: '4' }); + const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice); - const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id }); - assert.deepStrictEqual(res.body, [note3, note2, note1]); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); }); }); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index 4e3098e5b4..811ffc4b6e 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -1,24 +1,28 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, post, signup, uploadUrl } from '../utils.js'; +import { signup, api, post, uploadUrl, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('users/notes', () => { - let alice: misskey.entities.SignupResponse; - let jpgNote: misskey.entities.Note; - let pngNote: misskey.entities.Note; - let jpgPngNote: misskey.entities.Note; + let app: INestApplicationContext; + + let alice: misskey.entities.MeSignup; + let jpgNote: any; + let pngNote: any; + let jpgPngNote: any; beforeAll(async () => { + app = await startServer(); alice = await signup({ username: 'alice' }); - const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/192.jpg'); - const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/192.png'); + const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/Lenna.jpg'); + const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/Lenna.png'); jpgNote = await post(alice, { fileIds: [jpg.id], }); @@ -30,8 +34,12 @@ describe('users/notes', () => { }); }, 1000 * 60 * 2); + afterAll(async() => { + await app.close(); + }); + test('withFiles', async () => { - const res = await api('users/notes', { + const res = await api('/users/notes', { userId: alice.id, withFiles: true, }, alice); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index b0598fd578..5fa6f9a1c6 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,8 +8,20 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { inspect } from 'node:util'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { + signup, + post, + page, + role, + startServer, + api, + successfulApiCall, + failedApiCall, + uploadFile, +} from '../utils.js'; import type * as misskey from 'cherrypick-js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('ユーザー', () => { // エンティティとしてのユーザーを主眼においたテストを記述する @@ -24,12 +36,31 @@ describe('ユーザー', () => { }, {}); }; - const show = async (id: string, me = root): Promise => { - return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }); + // BUG cherrypick-jsとjson-schemaと実際に返ってくるデータが全部違う + type UserLite = misskey.entities.UserLite & { + badgeRoles: any[], + }; + + type UserDetailedNotMe = UserLite & + misskey.entities.UserDetailed & { + roles: any[], + }; + + type MeDetailed = UserDetailedNotMe & + misskey.entities.MeDetailed & { + achievements: object[], + loggedInDays: number, + policies: object, + }; + + type User = MeDetailed & { token: string }; + + const show = async (id: string, me = root): Promise => { + return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any; }; // UserLiteのキーが過不足なく入っている? - const userLite = (user: misskey.entities.UserLite): Partial => { + const userLite = (user: User): Partial => { return stripUndefined({ id: user.id, name: user.name, @@ -52,7 +83,7 @@ describe('ユーザー', () => { }; // UserDetailedNotMeのキーが過不足なく入っている? - const userDetailedNotMe = (user: misskey.entities.SignupResponse): Partial => { + const userDetailedNotMe = (user: User): Partial => { return stripUndefined({ ...userLite(user), url: user.url, @@ -92,7 +123,7 @@ describe('ユーザー', () => { }; // Relations関連のキーが過不足なく入っている? - const userDetailedNotMeWithRelations = (user: misskey.entities.SignupResponse): Partial => { + const userDetailedNotMeWithRelations = (user: User): Partial => { return stripUndefined({ ...userDetailedNotMe(user), isFollowing: user.isFollowing ?? false, @@ -109,7 +140,7 @@ describe('ユーザー', () => { }; // MeDetailedのキーが過不足なく入っている? - const meDetailed = (user: misskey.entities.SignupResponse, security = false): Partial => { + const meDetailed = (user: User, security = false): Partial => { return stripUndefined({ ...userDetailedNotMe(user), avatarId: user.avatarId, @@ -141,7 +172,6 @@ describe('ユーザー', () => { mutedWords: user.mutedWords, hardMutedWords: user.hardMutedWords, mutedInstances: user.mutedInstances, - // @ts-expect-error 後方互換性 mutingNotificationTypes: user.mutingNotificationTypes, notificationRecieveConfig: user.notificationRecieveConfig, emailNotificationTypes: user.emailNotificationTypes, @@ -156,53 +186,67 @@ describe('ユーザー', () => { }); }; - let root: misskey.entities.SignupResponse; - let alice: misskey.entities.SignupResponse; + let app: INestApplicationContext; + + let root: User; + let alice: User; let aliceNote: misskey.entities.Note; + let alicePage: misskey.entities.Page; + let aliceList: misskey.entities.UserList; + + let bob: User; + let bobNote: misskey.entities.Note; + + let carol: User; + let dave: User; + let ellen: User; + let frank: User; + + let usersReplying: User[]; + + let userNoNote: User; + let userNotExplorable: User; + let userLocking: User; + let userAdmin: User; + let roleAdmin: any; + let userModerator: User; + let roleModerator: any; + let userRolePublic: User; + let rolePublic: any; + let userRoleBadge: User; + let roleBadge: any; + let userSilenced: User; + let roleSilenced: any; + let userSuspended: User; + let userDeletedBySelf: User; + let userDeletedByAdmin: User; + let userFollowingAlice: User; + let userFollowedByAlice: User; + let userBlockingAlice: User; + let userBlockedByAlice: User; + let userMutingAlice: User; + let userMutedByAlice: User; + let userRnMutingAlice: User; + let userRnMutedByAlice: User; + let userFollowRequesting: User; + let userFollowRequested: User; - let bob: misskey.entities.SignupResponse; - - // NOTE: これがないと落ちる(bob の updatedAt が null になってしまうため?) - let bobNote: misskey.entities.Note; // eslint-disable-line @typescript-eslint/no-unused-vars - - let carol: misskey.entities.SignupResponse; - - let usersReplying: misskey.entities.SignupResponse[]; - - let userNoNote: misskey.entities.SignupResponse; - let userNotExplorable: misskey.entities.SignupResponse; - let userLocking: misskey.entities.SignupResponse; - let userAdmin: misskey.entities.SignupResponse; - let roleAdmin: misskey.entities.Role; - let userModerator: misskey.entities.SignupResponse; - let roleModerator: misskey.entities.Role; - let userRolePublic: misskey.entities.SignupResponse; - let rolePublic: misskey.entities.Role; - let userRoleBadge: misskey.entities.SignupResponse; - let roleBadge: misskey.entities.Role; - let userSilenced: misskey.entities.SignupResponse; - let roleSilenced: misskey.entities.Role; - let userSuspended: misskey.entities.SignupResponse; - let userDeletedBySelf: misskey.entities.SignupResponse; - let userDeletedByAdmin: misskey.entities.SignupResponse; - let userFollowingAlice: misskey.entities.SignupResponse; - let userFollowedByAlice: misskey.entities.SignupResponse; - let userBlockingAlice: misskey.entities.SignupResponse; - let userBlockedByAlice: misskey.entities.SignupResponse; - let userMutingAlice: misskey.entities.SignupResponse; - let userMutedByAlice: misskey.entities.SignupResponse; - let userRnMutingAlice: misskey.entities.SignupResponse; - let userRnMutedByAlice: misskey.entities.SignupResponse; - let userFollowRequesting: misskey.entities.SignupResponse; - let userFollowRequested: misskey.entities.SignupResponse; + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); - aliceNote = await post(alice, { text: 'test' }); + aliceNote = await post(alice, { text: 'test' }) as any; + alicePage = await page(alice); + aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body; bob = await signup({ username: 'bob' }); - bobNote = await post(bob, { text: 'test' }); + bobNote = await post(bob, { text: 'test' }) as any; carol = await signup({ username: 'carol' }); + dave = await signup({ username: 'dave' }); + ellen = await signup({ username: 'ellen' }); + frank = await signup({ username: 'frank' }); // @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => { @@ -213,7 +257,7 @@ describe('ユーザー', () => { } return (await acc).concat(u); - }, Promise.resolve([] as misskey.entities.SignupResponse[])); + }, Promise.resolve([] as User[])); userNoNote = await signup({ username: 'userNoNote' }); userNotExplorable = await signup({ username: 'userNotExplorable' }); @@ -232,7 +276,7 @@ describe('ユーザー', () => { rolePublic = await role(root, { isPublic: true, name: 'Public Role' }); await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root); userRoleBadge = await signup({ username: 'userRoleBadge' }); - roleBadge = await role(root, { asBadge: true, name: 'Badge Role', isPublic: true }); + roleBadge = await role(root, { asBadge: true, name: 'Badge Role' }); await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root); userSilenced = await signup({ username: 'userSilenced' }); await post(userSilenced, { text: 'test' }); @@ -278,10 +322,14 @@ describe('ユーザー', () => { await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting); }, 1000 * 60 * 10); + afterAll(async () => { + await app.close(); + }); + beforeEach(async () => { alice = { ...alice, - ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }), + ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any, }; aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice }); }); @@ -294,7 +342,7 @@ describe('ユーザー', () => { endpoint: 'signup', parameters: { username: 'zoe', password: 'password' }, user: undefined, - }) as unknown as misskey.entities.SignupResponse; // BUG MeDetailedに足りないキーがある + }) as unknown as User; // BUG MeDetailedに足りないキーがある // signupの時はtokenが含まれる特別なMeDetailedが返ってくる assert.match(response.token, /[a-zA-Z0-9]{16}/); @@ -304,7 +352,7 @@ describe('ユーザー', () => { assert.strictEqual(response.name, null); assert.strictEqual(response.username, 'zoe'); assert.strictEqual(response.host, null); - response.avatarUrl && assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); + assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.strictEqual(response.avatarBlurhash, null); assert.deepStrictEqual(response.avatarDecorations, []); assert.strictEqual(response.isBot, false); @@ -377,7 +425,6 @@ describe('ユーザー', () => { assert.deepStrictEqual(response.unreadAnnouncements, []); assert.deepStrictEqual(response.mutedWords, []); assert.deepStrictEqual(response.mutedInstances, []); - // @ts-expect-error 後方互換のため assert.deepStrictEqual(response.mutingNotificationTypes, []); assert.deepStrictEqual(response.notificationRecieveConfig, {}); assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest', 'groupInvited']); @@ -407,66 +454,66 @@ describe('ユーザー', () => { //#region 自分の情報の更新(i/update) test.each([ - { parameters: () => ({ name: null }) }, - { parameters: () => ({ name: 'x'.repeat(50) }) }, - { parameters: () => ({ name: 'x' }) }, - { parameters: () => ({ name: 'My name' }) }, - { parameters: () => ({ description: null }) }, - { parameters: () => ({ description: 'x'.repeat(1500) }) }, - { parameters: () => ({ description: 'x' }) }, - { parameters: () => ({ description: 'My description' }) }, - { parameters: () => ({ location: null }) }, - { parameters: () => ({ location: 'x'.repeat(50) }) }, - { parameters: () => ({ location: 'x' }) }, - { parameters: () => ({ location: 'My location' }) }, - { parameters: () => ({ birthday: '0000-00-00' }) }, - { parameters: () => ({ birthday: '9999-99-99' }) }, - { parameters: () => ({ lang: 'en-US' as const }) }, - { parameters: () => ({ fields: [] }) }, - { parameters: () => ({ fields: [{ name: 'x', value: 'x' }] }) }, - { parameters: () => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない - { parameters: () => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) }, - { parameters: () => ({ isLocked: true }) }, - { parameters: () => ({ isLocked: false }) }, - { parameters: () => ({ isExplorable: false }) }, - { parameters: () => ({ isExplorable: true }) }, - { parameters: () => ({ hideOnlineStatus: true }) }, - { parameters: () => ({ hideOnlineStatus: false }) }, - { parameters: () => ({ publicReactions: false }) }, - { parameters: () => ({ publicReactions: true }) }, - { parameters: () => ({ autoAcceptFollowed: true }) }, - { parameters: () => ({ autoAcceptFollowed: false }) }, - { parameters: () => ({ noCrawle: true }) }, - { parameters: () => ({ noCrawle: false }) }, - { parameters: () => ({ preventAiLearning: false }) }, - { parameters: () => ({ preventAiLearning: true }) }, - { parameters: () => ({ isBot: true }) }, - { parameters: () => ({ isBot: false }) }, - { parameters: () => ({ isCat: true }) }, - { parameters: () => ({ isCat: false }) }, - { parameters: () => ({ injectFeaturedNote: true }) }, - { parameters: () => ({ injectFeaturedNote: false }) }, - { parameters: () => ({ receiveAnnouncementEmail: true }) }, - { parameters: () => ({ receiveAnnouncementEmail: false }) }, - { parameters: () => ({ alwaysMarkNsfw: true }) }, - { parameters: () => ({ alwaysMarkNsfw: false }) }, - { parameters: () => ({ autoSensitive: true }) }, - { parameters: () => ({ autoSensitive: false }) }, - { parameters: () => ({ followingVisibility: 'private' as const }) }, - { parameters: () => ({ followingVisibility: 'followers' as const }) }, - { parameters: () => ({ followingVisibility: 'public' as const }) }, - { parameters: () => ({ followersVisibility: 'private' as const }) }, - { parameters: () => ({ followersVisibility: 'followers' as const }) }, - { parameters: () => ({ followersVisibility: 'public' as const }) }, - { parameters: () => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, - { parameters: () => ({ mutedWords: [['x'.repeat(194)]] }) }, - { parameters: () => ({ mutedWords: [] }) }, - { parameters: () => ({ mutedInstances: ['xxxx.xxxxx'] }) }, - { parameters: () => ({ mutedInstances: [] }) }, - { parameters: () => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) }, - { parameters: () => ({ notificationRecieveConfig: {} }) }, - { parameters: () => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) }, - { parameters: () => ({ emailNotificationTypes: [] }) }, + { parameters: (): object => ({ name: null }) }, + { parameters: (): object => ({ name: 'x'.repeat(50) }) }, + { parameters: (): object => ({ name: 'x' }) }, + { parameters: (): object => ({ name: 'My name' }) }, + { parameters: (): object => ({ description: null }) }, + { parameters: (): object => ({ description: 'x'.repeat(1500) }) }, + { parameters: (): object => ({ description: 'x' }) }, + { parameters: (): object => ({ description: 'My description' }) }, + { parameters: (): object => ({ location: null }) }, + { parameters: (): object => ({ location: 'x'.repeat(50) }) }, + { parameters: (): object => ({ location: 'x' }) }, + { parameters: (): object => ({ location: 'My location' }) }, + { parameters: (): object => ({ birthday: '0000-00-00' }) }, + { parameters: (): object => ({ birthday: '9999-99-99' }) }, + { parameters: (): object => ({ lang: 'en-US' }) }, + { parameters: (): object => ({ fields: [] }) }, + { parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) }, + { parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない + { parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) }, + { parameters: (): object => ({ isLocked: true }) }, + { parameters: (): object => ({ isLocked: false }) }, + { parameters: (): object => ({ isExplorable: false }) }, + { parameters: (): object => ({ isExplorable: true }) }, + { parameters: (): object => ({ hideOnlineStatus: true }) }, + { parameters: (): object => ({ hideOnlineStatus: false }) }, + { parameters: (): object => ({ publicReactions: false }) }, + { parameters: (): object => ({ publicReactions: true }) }, + { parameters: (): object => ({ autoAcceptFollowed: true }) }, + { parameters: (): object => ({ autoAcceptFollowed: false }) }, + { parameters: (): object => ({ noCrawle: true }) }, + { parameters: (): object => ({ noCrawle: false }) }, + { parameters: (): object => ({ preventAiLearning: false }) }, + { parameters: (): object => ({ preventAiLearning: true }) }, + { parameters: (): object => ({ isBot: true }) }, + { parameters: (): object => ({ isBot: false }) }, + { parameters: (): object => ({ isCat: true }) }, + { parameters: (): object => ({ isCat: false }) }, + { parameters: (): object => ({ injectFeaturedNote: true }) }, + { parameters: (): object => ({ injectFeaturedNote: false }) }, + { parameters: (): object => ({ receiveAnnouncementEmail: true }) }, + { parameters: (): object => ({ receiveAnnouncementEmail: false }) }, + { parameters: (): object => ({ alwaysMarkNsfw: true }) }, + { parameters: (): object => ({ alwaysMarkNsfw: false }) }, + { parameters: (): object => ({ autoSensitive: true }) }, + { parameters: (): object => ({ autoSensitive: false }) }, + { parameters: (): object => ({ followingVisibility: 'private' }) }, + { parameters: (): object => ({ followingVisibility: 'followers' }) }, + { parameters: (): object => ({ followingVisibility: 'public' }) }, + { parameters: (): object => ({ followersVisibility: 'private' }) }, + { parameters: (): object => ({ followersVisibility: 'followers' }) }, + { parameters: (): object => ({ followersVisibility: 'public' }) }, + { parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, + { parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, + { parameters: (): object => ({ mutedWords: [] }) }, + { parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) }, + { parameters: (): object => ({ mutedInstances: [] }) }, + { parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) }, + { parameters: (): object => ({ notificationRecieveConfig: {} }) }, + { parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest', 'groupInvited'] }) }, + { parameters: (): object => ({ emailNotificationTypes: [] }) }, ] as const)('を書き換えることができる($#)', async ({ parameters }) => { const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice }); const expected = { ...meDetailed(alice, true), ...parameters() }; @@ -475,13 +522,13 @@ describe('ユーザー', () => { test('を書き換えることができる(Avatar)', async () => { const aliceFile = (await uploadFile(alice)).body; - const parameters = { avatarId: aliceFile!.id }; + const parameters = { avatarId: aliceFile.id }; const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/); const expected = { ...meDetailed(alice, true), - avatarId: aliceFile!.id, + avatarId: aliceFile.id, avatarBlurhash: response.avatarBlurhash, avatarUrl: response.avatarUrl, }; @@ -500,13 +547,13 @@ describe('ユーザー', () => { test('を書き換えることができる(Banner)', async () => { const aliceFile = (await uploadFile(alice)).body; - const parameters = { bannerId: aliceFile!.id }; + const parameters = { bannerId: aliceFile.id }; const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/); const expected = { ...meDetailed(alice, true), - bannerId: aliceFile!.id, + bannerId: aliceFile.id, bannerBlurhash: response.bannerBlurhash, bannerUrl: response.bannerUrl, }; @@ -556,13 +603,13 @@ describe('ユーザー', () => { //#region ユーザー(users) test.each([ - { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: misskey.entities.UserLite): string => u.id }, - { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) }, - { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) }, - { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt }, - { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt }, - { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) }, - { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) }, + { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id }, + { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, + { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, ] as const)('をリスト形式で取得することができる($label)', async ({ parameters, selector }) => { const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); @@ -575,15 +622,15 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: () => userNotExplorable, excluded: true }, - { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true }, - { label: 'ブロックされているユーザーが含まれない', user: () => userBlockedByAlice, excluded: true }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice, excluded: true }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true }, + { label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true }, + { label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => { const parameters = { limit: 100 }; const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); @@ -597,44 +644,39 @@ describe('ユーザー', () => { //#region ユーザー情報(users/show) test.each([ - { label: 'ID指定で自分自身を', parameters: () => ({ userId: alice.id }), user: () => alice, type: meDetailed }, - { label: 'ID指定で他人を', parameters: () => ({ userId: alice.id }), user: () => bob, type: userDetailedNotMeWithRelations }, - { label: 'ID指定かつ未認証', parameters: () => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe }, - { label: '@指定で自分自身を', parameters: () => ({ username: alice.username }), user: () => alice, type: meDetailed }, - { label: '@指定で他人を', parameters: () => ({ username: alice.username }), user: () => bob, type: userDetailedNotMeWithRelations }, - { label: '@指定かつ未認証', parameters: () => ({ username: alice.username }), user: undefined, type: userDetailedNotMe }, + { label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed }, + { label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations }, + { label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe }, + { label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed }, + { label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations }, + { label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe }, ] as const)('を取得することができる($label)', async ({ parameters, user, type }) => { const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() }); const expected = type(alice); assert.deepStrictEqual(response, expected); }); test.each([ - { label: 'Administratorになっている', user: () => userAdmin, me: () => userAdmin, selector: (user: misskey.entities.MeDetailed) => user.isAdmin }, - // @ts-expect-error UserDetailedNotMe doesn't include isAdmin - { label: '自分以外から見たときはAdministratorか判定できない', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined }, - { label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator }, - // @ts-expect-error UserDetailedNotMe doesn't include isModerator - { label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined }, - { label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced }, - // FIXME: 落ちる - //{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended }, - { label: '削除済みになっている', user: () => userDeletedBySelf, me: () => userDeletedBySelf, selector: (user: misskey.entities.MeDetailed) => user.isDeleted }, - // @ts-expect-error UserDetailedNotMe doesn't include isDeleted - { label: '自分以外から見たときは削除済みか判定できない', user: () => userDeletedBySelf, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined }, - { label: '削除済み(byAdmin)になっている', user: () => userDeletedByAdmin, me: () => userDeletedByAdmin, selector: (user: misskey.entities.MeDetailed) => user.isDeleted }, - // @ts-expect-error UserDetailedNotMe doesn't include isDeleted - { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: () => userDeletedByAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined }, - { label: 'フォロー中になっている', user: () => userFollowedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowing }, - { label: 'フォローされている', user: () => userFollowingAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowed }, - { label: 'ブロック中になっている', user: () => userBlockedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocking }, - { label: 'ブロックされている', user: () => userBlockingAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocked }, - { label: 'ミュート中になっている', user: () => userMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isMuted }, - { label: 'リノートミュート中になっている', user: () => userRnMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isRenoteMuted }, - { label: 'フォローリクエスト中になっている', user: () => userFollowRequested, me: () => userFollowRequesting, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestFromYou }, - { label: 'フォローリクエストされている', user: () => userFollowRequesting, me: () => userFollowRequested, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestToYou }, + { label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin }, + { label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined }, + { label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator }, + { label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined }, + { label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced }, + //{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended }, + { label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted }, + { label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, + { label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted }, + { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, + { label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing }, + { label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed }, + { label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking }, + { label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked }, + { label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted }, + { label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted }, + { label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou }, + { label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou }, ] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => { const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice }); - assert.strictEqual(selector(response as any), (expected ?? ((): true => true))()); + assert.strictEqual(selector(response), (expected ?? ((): true => true))()); }); test('を取得することができ、Publicなロールがセットされていること', async () => { const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice }); @@ -657,16 +699,7 @@ describe('ユーザー', () => { iconUrl: roleBadge.iconUrl, displayOrder: roleBadge.displayOrder, }]); - assert.deepStrictEqual(response.roles, [{ - id: roleBadge.id, - name: roleBadge.name, - color: roleBadge.color, - iconUrl: roleBadge.iconUrl, - description: roleBadge.description, - isModerator: roleBadge.isModerator, - isAdministrator: roleBadge.isAdministrator, - displayOrder: roleBadge.displayOrder, - }]); + assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない }); test('をID指定のリスト形式で取得することができる(空)', async () => { const parameters = { userIds: [] }; @@ -685,18 +718,17 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: () => userSuspended, me: () => root }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root }, // BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる - //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: () => userSuspended, me: () => bob, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, - // @ts-expect-error excluded は上でコメントアウトされているので + //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => { const parameters = { userIds: [user().id] }; const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice }); @@ -721,15 +753,15 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => { const parameters = { query: user().username, limit: 1 }; const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); @@ -743,30 +775,30 @@ describe('ユーザー', () => { //#region ID指定検索(users/search-by-username-and-host) test.each([ - { label: '自分', parameters: { username: 'alice' }, user: () => [alice] }, - { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: () => [alice] }, - { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: () => [userFollowedByAlice] }, - { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: () => [] }, - { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: () => [bob] }, - { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: () => [bob] }, - { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: () => [bob] }, - { label: 'ローカル', parameters: { host: null, limit: 1 }, user: () => [userFollowedByAlice] }, - { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: () => [userFollowedByAlice] }, + { label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] }, + { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] }, + { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] }, + { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] }, + { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] }, + { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] }, + { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] }, + { label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] }, + { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] }, ])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => { const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); const expected = await Promise.all(user().map(u => show(u.id, alice))); assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => { const parameters = { username: user().username }; const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); @@ -788,15 +820,15 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - //{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + //{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => { const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0]; await post(alice, { text: `@${user().username} test`, replyId: replyTo.id }); @@ -810,12 +842,12 @@ describe('ユーザー', () => { //#region ハッシュタグ(hashtags/users) test.each([ - { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) }, - { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) }, - { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt }, - { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt }, - { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) }, - { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) }, + { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, + { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, ] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => { const hashtag = 'test_hashtag'; await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice }); @@ -829,15 +861,15 @@ describe('ユーザー', () => { assert.deepStrictEqual(response, expected); }); test.each([ - { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable }, - { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice }, - { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice }, - { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice }, - { label: '承認制ユーザーが含まれる', user: () => userLocking }, - { label: 'サイレンスユーザーが含まれる', user: () => userSilenced }, - { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true }, - { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin }, + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => { const hashtag = `user_test${user().username}`; if (user() !== userSuspended) { diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts index 1ae52f2b44..b30dead3e4 100644 --- a/packages/backend/test/e2e/well-known.ts +++ b/packages/backend/test/e2e/well-known.ts @@ -1,21 +1,29 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { host, origin, relativeFetch, signup } from '../utils.js'; +import { host, origin, relativeFetch, signup, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'cherrypick-js'; describe('.well-known', () => { + let app: INestApplicationContext; let alice: misskey.entities.User; beforeAll(async () => { + app = await startServer(); + alice = await signup({ username: 'alice' }); }, 1000 * 60 * 2); + afterAll(async () => { + await app.close(); + }); + test('nodeinfo', async () => { const res = await relativeFetch('.well-known/nodeinfo'); assert.ok(res.ok); diff --git a/packages/backend/test/eslint.config.js b/packages/backend/test/eslint.config.js deleted file mode 100644 index a0f43babad..0000000000 --- a/packages/backend/test/eslint.config.js +++ /dev/null @@ -1,22 +0,0 @@ -import globals from 'globals'; -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - }, -]; diff --git a/packages/backend/test/global.d.ts b/packages/backend/test/global.d.ts deleted file mode 100644 index 0363073356..0000000000 --- a/packages/backend/test/global.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type FIXME = any; diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts deleted file mode 100644 index 861bc6db66..0000000000 --- a/packages/backend/test/jest.setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { initTestDb, sendEnvResetRequest } from './utils.js'; - -beforeAll(async () => { - await Promise.all([ - initTestDb(false), - sendEnvResetRequest(), - ]); -}); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 3c7e796700..0ff4c29bc9 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -15,13 +15,7 @@ import type { LoggerService } from '@/core/LoggerService.js'; import type { MetaService } from '@/core/MetaService.js'; import type { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; -import type { - FollowRequestsRepository, - NoteReactionsRepository, - NotesRepository, - PollsRepository, - UsersRepository, -} from '@/models/_.js'; +import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; type MockResponse = { type: string; diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts index 7aa7a92702..a743badf9f 100644 --- a/packages/backend/test/prelude/get-api-validator.ts +++ b/packages/backend/test/prelude/get-api-validator.ts @@ -1,13 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import Ajv from 'ajv'; -import { Schema } from '@/misc/json-schema.js'; +import { Schema } from '@/misc/schema'; export const getValidator = (paramDef: Schema) => { - const ajv = new Ajv.default({ + const ajv = new Ajv({ useDefaults: true, }); ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); diff --git a/packages/backend/test/prelude/maybe.ts b/packages/backend/test/prelude/maybe.ts new file mode 100644 index 0000000000..a35b91d73f --- /dev/null +++ b/packages/backend/test/prelude/maybe.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as assert from 'assert'; +import { just, nothing } from '../../src/misc/prelude/maybe.js'; + +describe('just', () => { + test('has a value', () => { + assert.deepStrictEqual(just(3).isJust(), true); + }); + + test('has the inverse called get', () => { + assert.deepStrictEqual(just(3).get(), 3); + }); +}); + +describe('nothing', () => { + test('has no value', () => { + assert.deepStrictEqual(nothing().isJust(), false); + }); +}); diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts index b26ae09444..eca5702cd6 100644 --- a/packages/backend/test/prelude/url.ts +++ b/packages/backend/test/prelude/url.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/test/resources/192.jpg b/packages/backend/test/resources/192.jpg deleted file mode 100644 index 76374628e0acf2e3b85152d55c11c9a763027810..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5131 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 LNkvXXu0mjfqyVNd diff --git a/packages/backend/test/resources/Lenna.jpg b/packages/backend/test/resources/Lenna.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6b5b32281c1ffd5893d23392169cc368d72e0823 GIT binary patch literal 25360 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 z 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

Q*ie`|Cj&2i?YTA`Sqeun>bF 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 z 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ڵ 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 B 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 Vj64|zC_}ug_yjq)IN=xP{{mAaTcH2| diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index 2b562acda8..4597ff8780 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -5,7 +5,7 @@ "noImplicitAny": true, "noImplicitReturns": true, "noUnusedParameters": false, - "noUnusedLocals": false, + "noUnusedLocals": true, "noFallthroughCasesInSwitch": true, "declaration": false, "sourceMap": true, @@ -18,7 +18,6 @@ "strict": true, "strictNullChecks": true, "strictPropertyInitialization": false, - "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "resolveJsonModule": true, diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts deleted file mode 100644 index e971659070..0000000000 --- a/packages/backend/test/unit/AbuseReportNotificationService.ts +++ /dev/null @@ -1,343 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { jest } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; -import { - AbuseReportNotificationRecipientRepository, - MiAbuseReportNotificationRecipient, - MiSystemWebhook, - MiUser, - SystemWebhooksRepository, - UserProfilesRepository, - UsersRepository, -} from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { IdService } from '@/core/IdService.js'; -import { EmailService } from '@/core/EmailService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { randomString } from '../utils.js'; - -process.env.NODE_ENV = 'test'; - -describe('AbuseReportNotificationService', () => { - let app: TestingModule; - let service: AbuseReportNotificationService; - - // -------------------------------------------------------------------------------------- - - let usersRepository: UsersRepository; - let userProfilesRepository: UserProfilesRepository; - let systemWebhooksRepository: SystemWebhooksRepository; - let abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository; - let idService: IdService; - let roleService: jest.Mocked; - let emailService: jest.Mocked; - let webhookService: jest.Mocked; - - // -------------------------------------------------------------------------------------- - - let root: MiUser; - let alice: MiUser; - let bob: MiUser; - let systemWebhook1: MiSystemWebhook; - let systemWebhook2: MiSystemWebhook; - - // -------------------------------------------------------------------------------------- - - async function createUser(data: Partial = {}) { - const user = await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfilesRepository.insert({ - userId: user.id, - }); - - return user; - } - - async function createWebhook(data: Partial = {}) { - return systemWebhooksRepository - .insert({ - id: idService.gen(), - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - ...data, - }) - .then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0])); - } - - async function createRecipient(data: Partial = {}) { - return abuseReportNotificationRecipientRepository - .insert({ - id: idService.gen(), - isActive: true, - name: randomString(), - ...data, - }) - .then(x => abuseReportNotificationRecipientRepository.findOneByOrFail(x.identifiers[0])); - } - - // -------------------------------------------------------------------------------------- - - beforeAll(async () => { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - AbuseReportNotificationService, - IdService, - { - provide: RoleService, useFactory: () => ({ getModeratorIds: jest.fn() }), - }, - { - provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }), - }, - { - provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), - }, - { - provide: MetaService, useFactory: () => ({ fetch: jest.fn() }), - }, - { - provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }), - }, - { - provide: GlobalEventService, useFactory: () => ({ publishAdminStream: jest.fn() }), - }, - ], - }) - .compile(); - - usersRepository = app.get(DI.usersRepository); - userProfilesRepository = app.get(DI.userProfilesRepository); - systemWebhooksRepository = app.get(DI.systemWebhooksRepository); - abuseReportNotificationRecipientRepository = app.get(DI.abuseReportNotificationRecipientRepository); - - service = app.get(AbuseReportNotificationService); - idService = app.get(IdService); - roleService = app.get(RoleService) as jest.Mocked; - emailService = app.get(EmailService) as jest.Mocked; - webhookService = app.get(SystemWebhookService) as jest.Mocked; - - app.enableShutdownHooks(); - }); - - beforeEach(async () => { - root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); - alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false }); - bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false }); - systemWebhook1 = await createWebhook(); - systemWebhook2 = await createWebhook(); - - roleService.getModeratorIds.mockResolvedValue([root.id, alice.id, bob.id]); - }); - - afterEach(async () => { - emailService.sendEmail.mockClear(); - webhookService.enqueueSystemWebhook.mockClear(); - - await usersRepository.delete({}); - await userProfilesRepository.delete({}); - await systemWebhooksRepository.delete({}); - await abuseReportNotificationRecipientRepository.delete({}); - }); - - afterAll(async () => { - await app.close(); - }); - - // -------------------------------------------------------------------------------------- - - describe('createRecipient', () => { - test('作成成功1', async () => { - const params = { - isActive: true, - name: randomString(), - method: 'email' as RecipientMethod, - userId: alice.id, - systemWebhookId: null, - }; - - const recipient1 = await service.createRecipient(params, root); - expect(recipient1).toMatchObject(params); - }); - - test('作成成功2', async () => { - const params = { - isActive: true, - name: randomString(), - method: 'webhook' as RecipientMethod, - userId: null, - systemWebhookId: systemWebhook1.id, - }; - - const recipient1 = await service.createRecipient(params, root); - expect(recipient1).toMatchObject(params); - }); - }); - - describe('updateRecipient', () => { - test('更新成功1', async () => { - const recipient1 = await createRecipient({ - method: 'email', - userId: alice.id, - }); - - const params = { - id: recipient1.id, - isActive: false, - name: randomString(), - method: 'email' as RecipientMethod, - userId: bob.id, - systemWebhookId: null, - }; - - const recipient2 = await service.updateRecipient(params, root); - expect(recipient2).toMatchObject(params); - }); - - test('更新成功2', async () => { - const recipient1 = await createRecipient({ - method: 'webhook', - systemWebhookId: systemWebhook1.id, - }); - - const params = { - id: recipient1.id, - isActive: false, - name: randomString(), - method: 'webhook' as RecipientMethod, - userId: null, - systemWebhookId: systemWebhook2.id, - }; - - const recipient2 = await service.updateRecipient(params, root); - expect(recipient2).toMatchObject(params); - }); - }); - - describe('deleteRecipient', () => { - test('削除成功1', async () => { - const recipient1 = await createRecipient({ - method: 'email', - userId: alice.id, - }); - - await service.deleteRecipient(recipient1.id, root); - - await expect(abuseReportNotificationRecipientRepository.findOneBy({ id: recipient1.id })).resolves.toBeNull(); - }); - }); - - describe('fetchRecipients', () => { - async function create() { - const recipient1 = await createRecipient({ - method: 'email', - userId: alice.id, - }); - const recipient2 = await createRecipient({ - method: 'email', - userId: bob.id, - }); - - const recipient3 = await createRecipient({ - method: 'webhook', - systemWebhookId: systemWebhook1.id, - }); - const recipient4 = await createRecipient({ - method: 'webhook', - systemWebhookId: systemWebhook2.id, - }); - - return [recipient1, recipient2, recipient3, recipient4]; - } - - test('フィルタなし', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({}); - expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); - }); - - test('フィルタなし(非モデレータは除外される)', async () => { - roleService.getModeratorIds.mockClear(); - roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]); - - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({}); - // aliceはモデレータではないので除外される - expect(recipients).toEqual([recipient2, recipient3, recipient4]); - }); - - test('フィルタなし(非モデレータでも除外されないオプション設定)', async () => { - roleService.getModeratorIds.mockClear(); - roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]); - - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({}, { removeUnauthorized: false }); - expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); - }); - - test('emailのみ', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ method: ['email'] }); - expect(recipients).toEqual([recipient1, recipient2]); - }); - - test('webhookのみ', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ method: ['webhook'] }); - expect(recipients).toEqual([recipient3, recipient4]); - }); - - test('すべて', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ method: ['email', 'webhook'] }); - expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); - }); - - test('ID指定', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id] }); - expect(recipients).toEqual([recipient1, recipient3]); - }); - - test('ID指定(method=emailではないIDが混ざりこまない)', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['email'] }); - expect(recipients).toEqual([recipient1]); - }); - - test('ID指定(method=webhookではないIDが混ざりこまない)', async () => { - const [recipient1, recipient2, recipient3, recipient4] = await create(); - - const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['webhook'] }); - expect(recipients).toEqual([recipient3]); - }); - }); -}); diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts index 81da0fac31..99f9510907 100644 --- a/packages/backend/test/unit/AnnouncementService.ts +++ b/packages/backend/test/unit/AnnouncementService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,14 +10,7 @@ import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; -import type { - AnnouncementReadsRepository, - AnnouncementsRepository, - MiAnnouncement, - MiUser, - UsersRepository, -} from '@/models/_.js'; +import type { MiAnnouncement, AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository, MiUser } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { genAidx } from '@/misc/id/aidx.js'; import { CacheService } from '@/core/CacheService.js'; @@ -52,7 +45,7 @@ describe('AnnouncementService', () => { function createAnnouncement(data: Partial = {}) { return announcementsRepository.insert({ - id: genAidx(data.createdAt?.getTime() ?? Date.now()), + id: genAidx(data.createdAt ?? new Date()), updatedAt: null, title: 'Title', text: 'Text', @@ -68,7 +61,6 @@ describe('AnnouncementService', () => { ], providers: [ AnnouncementService, - AnnouncementEntityService, CacheService, IdService, ], diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts deleted file mode 100644 index f9978a1ab5..0000000000 --- a/packages/backend/test/unit/ApMfmService.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as assert from 'assert'; -import { Test } from '@nestjs/testing'; - -import { CoreModule } from '@/core/CoreModule.js'; -import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { MiNote } from '@/models/Note.js'; - -describe('ApMfmService', () => { - let apMfmService: ApMfmService; - - beforeAll(async () => { - const app = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule], - }).compile(); - apMfmService = app.get(ApMfmService); - }); - - describe('getNoteHtml', () => { - test('Do not provide _misskey_content for simple text', () => { - const note = { - text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com', - mentionedRemoteUsers: '[]', - }; - - const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); - - assert.equal(noMisskeyContent, true, 'noMisskeyContent'); - assert.equal(content, '

テキスト @mention 🍊 ​:emoji:​ https://example.com

', 'content'); - }); - - test('Provide _misskey_content for MFM', () => { - const note = { - text: '$[tada foo]', - mentionedRemoteUsers: '[]', - }; - - const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); - - assert.equal(noMisskeyContent, false, 'noMisskeyContent'); - assert.equal(content, '

foo

', 'content'); - }); - }); -}); diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts index 964c65ccaa..e50db1c01c 100644 --- a/packages/backend/test/unit/DriveService.ts +++ b/packages/backend/test/unit/DriveService.ts @@ -1,18 +1,12 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; -import { - DeleteObjectCommand, - DeleteObjectCommandOutput, - InvalidObjectState, - NoSuchKey, - S3Client, -} from '@aws-sdk/client-s3'; +import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3'; import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { DriveService } from '@/core/DriveService.js'; diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index bf8f3ab0e3..57d249e3b4 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -19,8 +19,8 @@ import { DI } from '@/di-symbols.js'; import type { TestingModule } from '@nestjs/testing'; function mockRedis() { - const hash = {} as any; - const set = jest.fn((key: string, value) => { + const hash = {}; + const set = jest.fn((key, value) => { const ret = hash[key]; hash[key] = value; return ret; @@ -56,13 +56,12 @@ describe('FetchInstanceMetadataService', () => { } else if (token === DI.redis) { return mockRedis; } - return null; }) .compile(); app.enableShutdownHooks(); - fetchInstanceMetadataService = app.get(FetchInstanceMetadataService) as jest.Mocked; + fetchInstanceMetadataService = app.get(FetchInstanceMetadataService); federatedInstanceService = app.get(FederatedInstanceService) as jest.Mocked; redisClient = app.get(DI.redis) as jest.Mocked; httpRequestService = app.get(HttpRequestService) as jest.Mocked; @@ -75,12 +74,11 @@ describe('FetchInstanceMetadataService', () => { test('Lock and update', async () => { redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } }); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' }); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); @@ -90,12 +88,11 @@ describe('FetchInstanceMetadataService', () => { test('Lock and don\'t update', async () => { redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now } }); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' }); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); @@ -104,33 +101,15 @@ describe('FetchInstanceMetadataService', () => { test('Do nothing when lock not acquired', async () => { redisClient.set = mockRedis(); - const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } }); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); - await fetchInstanceMetadataService.tryLock('example.com'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); - expect(tryLockSpy).toHaveBeenCalledTimes(1); + await fetchInstanceMetadataService.tryLock('example.com'); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' }); + expect(tryLockSpy).toHaveBeenCalledTimes(2); expect(unlockSpy).toHaveBeenCalledTimes(0); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); }); - - test('Do when lock not acquired but forced', async () => { - redisClient.set = mockRedis(); - const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); - httpRequestService.getJson.mockImplementation(() => { throw Error(); }); - await fetchInstanceMetadataService.tryLock('example.com'); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - - await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true); - expect(tryLockSpy).toHaveBeenCalledTimes(0); - expect(unlockSpy).toHaveBeenCalledTimes(1); - expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); - expect(httpRequestService.getJson).toHaveBeenCalled(); - }); }); diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index 29bd03a201..9e164fbfc9 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,12 +10,11 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; -import { afterAll, beforeAll, describe, test } from '@jest/globals'; +import { describe, beforeAll, afterAll, test } from '@jest/globals'; import { GlobalModule } from '@/GlobalModule.js'; -import { FileInfo, FileInfoService } from '@/core/FileInfoService.js'; +import { FileInfoService } from '@/core/FileInfoService.js'; //import { DI } from '@/di-symbols.js'; import { AiService } from '@/core/AiService.js'; -import { LoggerService } from '@/core/LoggerService.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -28,15 +27,6 @@ const moduleMocker = new ModuleMocker(global); describe('FileInfoService', () => { let app: TestingModule; let fileInfoService: FileInfoService; - const strip = (fileInfo: FileInfo): Omit, 'warnings' | 'blurhash' | 'sensitive' | 'porn'> => { - const fi: Partial = fileInfo; - delete fi.warnings; - delete fi.sensitive; - delete fi.blurhash; - delete fi.porn; - - return fi; - } beforeAll(async () => { app = await Test.createTestingModule({ @@ -45,7 +35,6 @@ describe('FileInfoService', () => { ], providers: [ AiService, - LoggerService, FileInfoService, ], }) @@ -72,7 +61,11 @@ describe('FileInfoService', () => { test('Empty file', async () => { const path = `${resources}/emptyfile`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 0, md5: 'd41d8cd98f00b204e9800998ecf8427e', @@ -88,24 +81,32 @@ describe('FileInfoService', () => { describe('IMAGE', () => { test('Generic JPEG', async () => { - const path = `${resources}/192.jpg`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const path = `${resources}/Lenna.jpg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { - size: 5131, - md5: '8c9ed0677dd2b8f9f7472c3af247e5e3', + size: 25360, + md5: '091b3f259662aa31e2ffef4519951168', type: { mime: 'image/jpeg', ext: 'jpg', }, - width: 192, - height: 192, + width: 512, + height: 512, orientation: undefined, }); }); test('Generic APNG', async () => { const path = `${resources}/anime.png`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 1868, md5: '08189c607bea3b952704676bb3c979e0', @@ -121,7 +122,11 @@ describe('FileInfoService', () => { test('Generic AGIF', async () => { const path = `${resources}/anime.gif`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 2248, md5: '32c47a11555675d9267aee1a86571e7e', @@ -137,7 +142,11 @@ describe('FileInfoService', () => { test('PNG with alpha', async () => { const path = `${resources}/with-alpha.png`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 3772, md5: 'f73535c3e1e27508885b69b10cf6e991', @@ -153,7 +162,11 @@ describe('FileInfoService', () => { test('Generic SVG', async () => { const path = `${resources}/image.svg`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 505, md5: 'b6f52b4b021e7b92cdd04509c7267965', @@ -170,7 +183,11 @@ describe('FileInfoService', () => { test('SVG with XML definition', async () => { // https://github.com/misskey-dev/misskey/issues/4413 const path = `${resources}/with-xml-def.svg`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 544, md5: '4b7a346cde9ccbeb267e812567e33397', @@ -186,7 +203,11 @@ describe('FileInfoService', () => { test('Dimension limit', async () => { const path = `${resources}/25000x25000.png`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 75933, md5: '268c5dde99e17cf8fe09f1ab3f97df56', @@ -202,7 +223,11 @@ describe('FileInfoService', () => { test('Rotate JPEG', async () => { const path = `${resources}/rotate.jpg`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; assert.deepStrictEqual(info, { size: 12624, md5: '68d5b2d8d1d1acbbce99203e3ec3857e', @@ -220,7 +245,11 @@ describe('FileInfoService', () => { describe('AUDIO', () => { test('MP3', async () => { const path = `${resources}/kick_gaba7.mp3`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; @@ -236,7 +265,11 @@ describe('FileInfoService', () => { test('WAV', async () => { const path = `${resources}/kick_gaba7.wav`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; @@ -252,7 +285,11 @@ describe('FileInfoService', () => { test('AAC', async () => { const path = `${resources}/kick_gaba7.aac`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; @@ -268,7 +305,11 @@ describe('FileInfoService', () => { test('FLAC', async () => { const path = `${resources}/kick_gaba7.flac`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; @@ -282,36 +323,27 @@ describe('FileInfoService', () => { }); }); - test('MPEG-4 AUDIO (M4A)', async () => { - const path = `${resources}/kick_gaba7.m4a`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); - delete info.width; - delete info.height; - delete info.orientation; - assert.deepStrictEqual(info, { - size: 9817, - md5: '74c9279a4abe98789565f1dc1a541a42', - type: { - mime: 'audio/mp4', - ext: 'm4a', - }, - }); - }); - + /* + * video/webmとして検出されてしまう test('WEBM AUDIO', async () => { const path = `${resources}/kick_gaba7.webm`; - const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true })); + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; delete info.width; delete info.height; delete info.orientation; assert.deepStrictEqual(info, { size: 8879, - md5: '53bc1adcb6acbbda67ff9bd484896438', + md5: '3350083dec312419cfdc06c16413aca7', type: { mime: 'audio/webm', ext: 'webm', }, }); }); + */ }); }); diff --git a/packages/backend/test/unit/MetaService.ts b/packages/backend/test/unit/MetaService.ts index 19c98eab3d..d3d84f4bd2 100644 --- a/packages/backend/test/unit/MetaService.ts +++ b/packages/backend/test/unit/MetaService.ts @@ -1,18 +1,20 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; +import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; +import type { MetasRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { CoreModule } from '@/core/CoreModule.js'; -import type { TestingModule } from '@nestjs/testing'; import type { DataSource } from 'typeorm'; +import type { TestingModule } from '@nestjs/testing'; describe('MetaService', () => { let app: TestingModule; diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 2bbe9a907a..c27067ff78 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -33,18 +33,6 @@ describe('MfmService', () => { const output = '

foo
bar
baz

'; assert.equal(mfmService.toHtml(mfm.parse(input)), output); }); - - test('Do not generate unnecessary span', () => { - const input = 'foo $[tada bar]'; - const output = '

foo bar

'; - assert.equal(mfmService.toHtml(mfm.parse(input)), output); - }); - - test('escape', () => { - const input = '```\n

Hello, world!

\n```'; - const output = '

<p>Hello, world!</p>

'; - assert.equal(mfmService.toHtml(mfm.parse(input)), output); - }); }); describe('fromHtml', () => { diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts deleted file mode 100644 index f2d4c8ffbb..0000000000 --- a/packages/backend/test/unit/NoteCreateService.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test } from '@nestjs/testing'; - -import { CoreModule } from '@/core/CoreModule.js'; -import { NoteCreateService } from '@/core/NoteCreateService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { MiNote } from '@/models/Note.js'; -import { IPoll } from '@/models/Poll.js'; -import { MiDriveFile } from '@/models/DriveFile.js'; - -describe('NoteCreateService', () => { - let noteCreateService: NoteCreateService; - - beforeAll(async () => { - const app = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule], - }).compile(); - noteCreateService = app.get(NoteCreateService); - }); - - describe('is-renote', () => { - const base: MiNote = { - id: 'some-note-id', - replyId: null, - reply: null, - renoteId: null, - renote: null, - threadId: null, - text: null, - name: null, - cw: null, - userId: 'some-user-id', - user: null, - localOnly: false, - reactionAcceptance: null, - renoteCount: 0, - repliesCount: 0, - clippedCount: 0, - reactions: {}, - visibility: 'public', - uri: null, - url: null, - fileIds: [], - attachedFileTypes: [], - visibleUserIds: [], - mentions: [], - mentionedRemoteUsers: '', - reactionAndUserPairCache: [], - emojis: [], - tags: [], - hasPoll: false, - channelId: null, - channel: null, - userHost: null, - replyUserId: null, - replyUserHost: null, - renoteUserId: null, - renoteUserHost: null, - }; - - const poll: IPoll = { - choices: ['kinoko', 'takenoko'], - multiple: false, - expiresAt: null, - }; - - const file: MiDriveFile = { - id: 'some-file-id', - userId: null, - user: null, - userHost: null, - md5: '', - name: '', - type: '', - size: 0, - comment: null, - blurhash: null, - properties: {}, - storedInternal: false, - url: '', - thumbnailUrl: null, - webpublicUrl: null, - webpublicType: null, - accessKey: null, - thumbnailAccessKey: null, - webpublicAccessKey: null, - uri: null, - src: null, - folderId: null, - folder: null, - isSensitive: false, - maybeSensitive: false, - maybePorn: false, - isLink: false, - requestHeaders: null, - requestIp: null, - }; - - test('note without renote should not be Renote', () => { - const note = { renote: null }; - expect(noteCreateService['isRenote'](note)).toBe(false); - }); - - test('note with renote should be Renote and not be Quote', () => { - const note = { renote: base }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(false); - }); - - test('note with renote and text should be Quote', () => { - const note = { renote: base, text: 'some-text' }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - - test('note with renote and cw should be Quote', () => { - const note = { renote: base, cw: 'some-cw' }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - - test('note with renote and reply should be Quote', () => { - const note = { renote: base, reply: { ...base, id: 'another-note-id' } }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - - test('note with renote and poll should be Quote', () => { - const note = { renote: base, poll }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - - test('note with renote and non-empty files should be Quote', () => { - const note = { renote: base, files: [file] }; - expect(noteCreateService['isRenote'](note)).toBe(true); - expect(noteCreateService['isQuote'](note)).toBe(true); - }); - }); -}); diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index 1957f4544c..95565432b7 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -90,45 +90,4 @@ describe('ReactionService', () => { assert.strictEqual(await reactionService.normalize('unknown'), '❤'); }); }); - - describe('convertLegacyReactions', () => { - test('空の入力に対しては何もしない', () => { - const input = {}; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); - }); - - test('Unicode絵文字リアクションを変換してしまわない', () => { - const input = { '👍': 1, '🍮': 2 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); - }); - - test('カスタム絵文字リアクションを変換してしまわない', () => { - const input = { ':like@.:': 1, ':pudding@example.tld:': 2 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); - }); - - test('文字列によるレガシーなリアクションを変換する', () => { - const input = { 'like': 1, 'pudding': 2 }; - const output = { '👍': 1, '🍮': 2 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); - }); - - test('host部分が省略されたレガシーなカスタム絵文字リアクションを変換する', () => { - const input = { ':custom_emoji:': 1 }; - const output = { ':custom_emoji@.:': 1 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); - }); - - test('「0個のリアクション」情報を削除する', () => { - const input = { 'angry': 0 }; - const output = {}; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); - }); - - test('host部分の有無によりデコードすると同じ表記になるカスタム絵文字リアクションの個数情報を正しく足し合わせる', () => { - const input = { ':custom_emoji:': 1, ':custom_emoji@.:': 2 }; - const output = { ':custom_emoji@.:': 3 }; - assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); - }); - }); }); diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts index 9676abf07b..dd71636161 100644 --- a/packages/backend/test/unit/RelayService.ts +++ b/packages/backend/test/unit/RelayService.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -90,8 +90,7 @@ describe('RelayService', () => { expect(queueService.deliver).toHaveBeenCalled(); expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo'); - expect(typeof queueService.deliver.mock.lastCall![1]?.object).toBe('object'); - expect((queueService.deliver.mock.lastCall![1]?.object as any).type).toBe('Follow'); + expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow'); expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com'); //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index b6cbe4c520..b887b9dd03 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -1,25 +1,17 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; -import { setTimeout } from 'node:timers/promises'; import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; -import { - MiRole, - MiRoleAssignment, - MiUser, - RoleAssignmentsRepository, - RolesRepository, - UsersRepository, -} from '@/models/_.js'; +import type { MiRole, RolesRepository, RoleAssignmentsRepository, UsersRepository, MiUser } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { genAidx } from '@/misc/id/aidx.js'; @@ -28,8 +20,7 @@ import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { NotificationService } from '@/core/NotificationService.js'; -import { RoleCondFormulaValue } from '@/models/Role.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { sleep } from '../utils.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -45,54 +36,26 @@ describe('RoleService', () => { let notificationService: jest.Mocked; let clock: lolex.InstalledClock; - async function createUser(data: Partial = {}) { + function createUser(data: Partial = {}) { const un = secureRndstr(16); - const x = await usersRepository.insert({ + return usersRepository.insert({ id: genAidx(Date.now()), username: un, usernameLower: un, ...data, - }); - return await usersRepository.findOneByOrFail(x.identifiers[0]); + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); } - async function createRole(data: Partial = {}) { - const x = await rolesRepository.insert({ + function createRole(data: Partial = {}) { + return rolesRepository.insert({ id: genAidx(Date.now()), updatedAt: new Date(), lastUsedAt: new Date(), - name: '', description: '', ...data, - }); - return await rolesRepository.findOneByOrFail(x.identifiers[0]); - } - - function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial = {}) { - return createRole({ - name: `[conditional] ${condFormula.type}`, - target: 'conditional', - condFormula: condFormula, - ...data, - }); - } - - async function assignRole(args: Partial) { - const id = genAidx(Date.now()); - const expiresAt = new Date(); - expiresAt.setDate(expiresAt.getDate() + 1); - - await roleAssignmentsRepository.insert({ - id, - expiresAt, - ...args, - }); - - return await roleAssignmentsRepository.findOneByOrFail({ id }); - } - - function aidx() { - return genAidx(Date.now()); + }) + .then(x => rolesRepository.findOneByOrFail(x.identifiers[0])); } beforeEach(async () => { @@ -110,7 +73,6 @@ describe('RoleService', () => { CacheService, IdService, GlobalEventService, - UserEntityService, { provide: NotificationService, useFactory: () => ({ @@ -247,6 +209,48 @@ describe('RoleService', () => { expect(result.driveCapacityMb).toBe(100); }); + test('conditional role', async () => { + const user1 = await createUser({ + id: genAidx(Date.now() - (1000 * 60 * 60 * 24 * 365)), + }); + const user2 = await createUser({ + id: genAidx(Date.now() - (1000 * 60 * 60 * 24 * 365)), + followersCount: 10, + }); + await createRole({ + name: 'a', + policies: { + canManageCustomEmojis: { + useDefault: false, + priority: 0, + value: true, + }, + }, + target: 'conditional', + condFormula: { + type: 'and', + values: [{ + type: 'followersMoreThanOrEq', + value: 10, + }, { + type: 'createdMoreThan', + sec: 60 * 60 * 24 * 7, + }], + }, + }); + + metaService.fetch.mockResolvedValue({ + policies: { + canManageCustomEmojis: false, + }, + } as any); + + const user1Policies = await roleService.getUserPolicies(user1.id); + const user2Policies = await roleService.getUserPolicies(user2.id); + expect(user1Policies.canManageCustomEmojis).toBe(false); + expect(user2Policies.canManageCustomEmojis).toBe(true); + }); + test('expired role', async () => { const user = await createUser(); const role = await createRole({ @@ -278,524 +282,13 @@ describe('RoleService', () => { // ストリーミング経由で反映されるまでちょっと待つ clock.uninstall(); - await setTimeout(100); + await sleep(100); const resultAfter25hAgain = await roleService.getUserPolicies(user.id); expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); }); }); - describe('getModeratorIds', () => { - test('includeAdmins = false, excludeExpire = false', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds(false, false); - expect(result).toEqual([modeUser1.id, modeUser2.id]); - }); - - test('includeAdmins = false, excludeExpire = true', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds(false, true); - expect(result).toEqual([modeUser1.id]); - }); - - test('includeAdmins = true, excludeExpire = false', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds(true, false); - expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]); - }); - - test('includeAdmins = true, excludeExpire = true', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds(true, true); - expect(result).toEqual([adminUser1.id, modeUser1.id]); - }); - }); - - describe('conditional role', () => { - test('~かつ~', async () => { - const [user1, user2, user3, user4] = await Promise.all([ - createUser({ isBot: true, isCat: false, isSuspended: false }), - createUser({ isBot: false, isCat: true, isSuspended: false }), - createUser({ isBot: true, isCat: true, isSuspended: false }), - createUser({ isBot: false, isCat: false, isSuspended: true }), - ]); - const role1 = await createConditionalRole({ - id: aidx(), - type: 'isBot', - }); - const role2 = await createConditionalRole({ - id: aidx(), - type: 'isCat', - }); - const role3 = await createConditionalRole({ - id: aidx(), - type: 'isSuspended', - }); - const role4 = await createConditionalRole({ - id: aidx(), - type: 'and', - values: [role1.condFormula, role2.condFormula], - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - const actual4 = await roleService.getUserRoles(user4.id); - expect(actual1.some(r => r.id === role4.id)).toBe(false); - expect(actual2.some(r => r.id === role4.id)).toBe(false); - expect(actual3.some(r => r.id === role4.id)).toBe(true); - expect(actual4.some(r => r.id === role4.id)).toBe(false); - }); - - test('~または~', async () => { - const [user1, user2, user3, user4] = await Promise.all([ - createUser({ isBot: true, isCat: false, isSuspended: false }), - createUser({ isBot: false, isCat: true, isSuspended: false }), - createUser({ isBot: true, isCat: true, isSuspended: false }), - createUser({ isBot: false, isCat: false, isSuspended: true }), - ]); - const role1 = await createConditionalRole({ - id: aidx(), - type: 'isBot', - }); - const role2 = await createConditionalRole({ - id: aidx(), - type: 'isCat', - }); - const role3 = await createConditionalRole({ - id: aidx(), - type: 'isSuspended', - }); - const role4 = await createConditionalRole({ - id: aidx(), - type: 'or', - values: [role1.condFormula, role2.condFormula], - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - const actual4 = await roleService.getUserRoles(user4.id); - expect(actual1.some(r => r.id === role4.id)).toBe(true); - expect(actual2.some(r => r.id === role4.id)).toBe(true); - expect(actual3.some(r => r.id === role4.id)).toBe(true); - expect(actual4.some(r => r.id === role4.id)).toBe(false); - }); - - test('~ではない', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ isBot: true, isCat: false, isSuspended: false }), - createUser({ isBot: false, isCat: true, isSuspended: false }), - createUser({ isBot: true, isCat: true, isSuspended: false }), - ]); - const role1 = await createConditionalRole({ - id: aidx(), - type: 'isBot', - }); - const role2 = await createConditionalRole({ - id: aidx(), - type: 'isCat', - }); - const role4 = await createConditionalRole({ - id: aidx(), - type: 'not', - value: role1.condFormula, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role4.id)).toBe(false); - expect(actual2.some(r => r.id === role4.id)).toBe(true); - expect(actual3.some(r => r.id === role4.id)).toBe(false); - }); - - test('マニュアルロールにアサイン済み', async () => { - const [user1, user2, role1] = await Promise.all([ - createUser(), - createUser(), - createRole({ - name: 'manual role', - }), - ]); - const role2 = await createConditionalRole({ - id: aidx(), - type: 'roleAssignedTo', - roleId: role1.id, - }); - await roleService.assign(user2.id, role1.id); - - const [u1role, u2role] = await Promise.all([ - roleService.getUserRoles(user1.id), - roleService.getUserRoles(user2.id), - ]); - expect(u1role.some(r => r.id === role2.id)).toBe(false); - expect(u2role.some(r => r.id === role2.id)).toBe(true); - }); - - test('ローカルユーザのみ', async () => { - const [user1, user2] = await Promise.all([ - createUser({ host: null }), - createUser({ host: 'example.com' }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isLocal', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(false); - }); - - test('リモートユーザのみ', async () => { - const [user1, user2] = await Promise.all([ - createUser({ host: null }), - createUser({ host: 'example.com' }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isRemote', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('サスペンド済みユーザである', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isSuspended: false }), - createUser({ isSuspended: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isSuspended', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('鍵アカウントユーザである', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isLocked: false }), - createUser({ isLocked: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isLocked', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('botユーザである', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isBot: false }), - createUser({ isBot: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isBot', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('猫である', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isCat: false }), - createUser({ isCat: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isCat', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('「ユーザを見つけやすくする」が有効なアカウント', async () => { - const [user1, user2] = await Promise.all([ - createUser({ isExplorable: false }), - createUser({ isExplorable: true }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'isExplorable', - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - }); - - test('ユーザが作成されてから指定期間経過した', async () => { - const base = new Date(); - base.setMinutes(base.getMinutes() - 5); - - const d1 = new Date(base); - const d2 = new Date(base); - const d3 = new Date(base); - d1.setSeconds(d1.getSeconds() - 1); - d3.setSeconds(d3.getSeconds() + 1); - - const [user1, user2, user3] = await Promise.all([ - // 4:59 - createUser({ id: genAidx(d1.getTime()) }), - // 5:00 - createUser({ id: genAidx(d2.getTime()) }), - // 5:01 - createUser({ id: genAidx(d3.getTime()) }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'createdLessThan', - // 5 minutes - sec: 300, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(false); - expect(actual3.some(r => r.id === role.id)).toBe(true); - }); - - test('ユーザが作成されてから指定期間経っていない', async () => { - const base = new Date(); - base.setMinutes(base.getMinutes() - 5); - - const d1 = new Date(base); - const d2 = new Date(base); - const d3 = new Date(base); - d1.setSeconds(d1.getSeconds() - 1); - d3.setSeconds(d3.getSeconds() + 1); - - const [user1, user2, user3] = await Promise.all([ - // 4:59 - createUser({ id: genAidx(d1.getTime()) }), - // 5:00 - createUser({ id: genAidx(d2.getTime()) }), - // 5:01 - createUser({ id: genAidx(d3.getTime()) }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'createdMoreThan', - // 5 minutes - sec: 300, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(false); - expect(actual3.some(r => r.id === role.id)).toBe(false); - }); - - test('フォロワー数が指定値以下', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ followersCount: 99 }), - createUser({ followersCount: 100 }), - createUser({ followersCount: 101 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'followersLessThanOrEq', - value: 100, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(false); - }); - - test('フォロワー数が指定値以下', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ followersCount: 99 }), - createUser({ followersCount: 100 }), - createUser({ followersCount: 101 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'followersMoreThanOrEq', - value: 100, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(true); - }); - - test('フォロー数が指定値以下', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ followingCount: 99 }), - createUser({ followingCount: 100 }), - createUser({ followingCount: 101 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'followingLessThanOrEq', - value: 100, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(false); - }); - - test('フォロー数が指定値以上', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ followingCount: 99 }), - createUser({ followingCount: 100 }), - createUser({ followingCount: 101 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'followingMoreThanOrEq', - value: 100, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(true); - }); - - test('ノート数が指定値以下', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ notesCount: 9 }), - createUser({ notesCount: 10 }), - createUser({ notesCount: 11 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'notesLessThanOrEq', - value: 10, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(true); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(false); - }); - - test('ノート数が指定値以上', async () => { - const [user1, user2, user3] = await Promise.all([ - createUser({ notesCount: 9 }), - createUser({ notesCount: 10 }), - createUser({ notesCount: 11 }), - ]); - const role = await createConditionalRole({ - id: aidx(), - type: 'notesMoreThanOrEq', - value: 10, - }); - - const actual1 = await roleService.getUserRoles(user1.id); - const actual2 = await roleService.getUserRoles(user2.id); - const actual3 = await roleService.getUserRoles(user3.id); - expect(actual1.some(r => r.id === role.id)).toBe(false); - expect(actual2.some(r => r.id === role.id)).toBe(true); - expect(actual3.some(r => r.id === role.id)).toBe(true); - }); - }); - describe('assign', () => { test('公開ロールの場合は通知される', async () => { const user = await createUser(); @@ -807,7 +300,7 @@ describe('RoleService', () => { await roleService.assign(user.id, role.id); clock.uninstall(); - await setTimeout(100); + await sleep(100); const assignments = await roleAssignmentsRepository.find({ where: { @@ -835,7 +328,7 @@ describe('RoleService', () => { await roleService.assign(user.id, role.id); clock.uninstall(); - await setTimeout(100); + await sleep(100); const assignments = await roleAssignmentsRepository.find({ where: { diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts index 9cde506ea7..fe2cb671e0 100644 --- a/packages/backend/test/unit/S3Service.ts +++ b/packages/backend/test/unit/S3Service.ts @@ -1,18 +1,12 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; -import { - CompleteMultipartUploadCommand, - CreateMultipartUploadCommand, - PutObjectCommand, - S3Client, - UploadPartCommand, -} from '@aws-sdk/client-s3'; +import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts deleted file mode 100644 index 790cd1490e..0000000000 --- a/packages/backend/test/unit/SystemWebhookService.ts +++ /dev/null @@ -1,516 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { setTimeout } from 'node:timers/promises'; -import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; -import { Test, TestingModule } from '@nestjs/testing'; -import { MiUser } from '@/models/User.js'; -import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; -import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; -import { QueueService } from '@/core/QueueService.js'; -import { LoggerService } from '@/core/LoggerService.js'; -import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { randomString } from '../utils.js'; - -describe('SystemWebhookService', () => { - let app: TestingModule; - let service: SystemWebhookService; - - // -------------------------------------------------------------------------------------- - - let usersRepository: UsersRepository; - let systemWebhooksRepository: SystemWebhooksRepository; - let idService: IdService; - let queueService: jest.Mocked; - - // -------------------------------------------------------------------------------------- - - let root: MiUser; - - // -------------------------------------------------------------------------------------- - - async function createUser(data: Partial = {}) { - return await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - } - - async function createWebhook(data: Partial = {}) { - return systemWebhooksRepository - .insert({ - id: idService.gen(), - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - ...data, - }) - .then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0])); - } - - // -------------------------------------------------------------------------------------- - - async function beforeAllImpl() { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - SystemWebhookService, - IdService, - LoggerService, - GlobalEventService, - { - provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }), - }, - { - provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }), - }, - ], - }) - .compile(); - - usersRepository = app.get(DI.usersRepository); - systemWebhooksRepository = app.get(DI.systemWebhooksRepository); - - service = app.get(SystemWebhookService); - idService = app.get(IdService); - queueService = app.get(QueueService) as jest.Mocked; - - app.enableShutdownHooks(); - } - - async function afterAllImpl() { - await app.close(); - } - - async function beforeEachImpl() { - root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' }); - } - - async function afterEachImpl() { - await usersRepository.delete({}); - await systemWebhooksRepository.delete({}); - } - - // -------------------------------------------------------------------------------------- - - describe('アプリを毎回作り直す必要のないグループ', () => { - beforeAll(beforeAllImpl); - afterAll(afterAllImpl); - beforeEach(beforeEachImpl); - afterEach(afterEachImpl); - - describe('fetchSystemWebhooks', () => { - test('フィルタなし', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks(); - expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]); - }); - - test('activeのみ', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ isActive: true }); - expect(fetchedWebhooks).toEqual([webhook1, webhook3]); - }); - - test('特定のイベントのみ', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'] }); - expect(fetchedWebhooks).toEqual([webhook1, webhook2]); - }); - - test('activeな特定のイベントのみ', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'], isActive: true }); - expect(fetchedWebhooks).toEqual([webhook1]); - }); - - test('ID指定', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id] }); - expect(fetchedWebhooks).toEqual([webhook1, webhook4]); - }); - - test('ID指定(他条件とANDになるか見たい)', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - const webhook2 = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - const webhook3 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - const webhook4 = await createWebhook({ - isActive: false, - on: [], - }); - - const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false }); - expect(fetchedWebhooks).toEqual([webhook4]); - }); - }); - - describe('createSystemWebhook', () => { - test('作成成功 ', async () => { - const params = { - isActive: true, - name: randomString(), - on: ['abuseReport'] as SystemWebhookEventType[], - url: 'https://example.com', - secret: randomString(), - }; - - const webhook = await service.createSystemWebhook(params, root); - expect(webhook).toMatchObject(params); - }); - }); - - describe('updateSystemWebhook', () => { - test('更新成功', async () => { - const webhook = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - - const params = { - id: webhook.id, - isActive: false, - name: randomString(), - on: ['abuseReport'] as SystemWebhookEventType[], - url: randomString(), - secret: randomString(), - }; - - const updatedWebhook = await service.updateSystemWebhook(params, root); - expect(updatedWebhook).toMatchObject(params); - }); - }); - - describe('deleteSystemWebhook', () => { - test('削除成功', async () => { - const webhook = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - - await service.deleteSystemWebhook(webhook.id, root); - - await expect(systemWebhooksRepository.findOneBy({ id: webhook.id })).resolves.toBeNull(); - }); - }); - }); - - describe('アプリを毎回作り直す必要があるグループ', () => { - beforeEach(async () => { - await beforeAllImpl(); - await beforeEachImpl(); - }); - - afterEach(async () => { - await afterEachImpl(); - await afterAllImpl(); - }); - - describe('enqueueSystemWebhook', () => { - test('キューに追加成功', async () => { - const webhook = await createWebhook({ - isActive: true, - on: ['abuseReport'], - }); - await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' }); - - expect(queueService.systemWebhookDeliver).toHaveBeenCalled(); - }); - - test('非アクティブなWebhookはキューに追加されない', async () => { - const webhook = await createWebhook({ - isActive: false, - on: ['abuseReport'], - }); - await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' }); - - expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); - }); - - test('未許可のイベント種別が渡された場合はWebhookはキューに追加されない', async () => { - const webhook1 = await createWebhook({ - isActive: true, - on: [], - }); - const webhook2 = await createWebhook({ - isActive: true, - on: ['abuseReportResolved'], - }); - await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' }); - await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' }); - - expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); - }); - }); - - describe('fetchActiveSystemWebhooks', () => { - describe('systemWebhookCreated', () => { - test('ActiveなWebhookが追加された時、キャッシュに追加されている', async () => { - const webhook = await service.createSystemWebhook( - { - isActive: true, - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks).toEqual([webhook]); - }); - - test('NotActiveなWebhookが追加された時、キャッシュに追加されていない', async () => { - const webhook = await service.createSystemWebhook( - { - isActive: false, - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks).toEqual([]); - }); - }); - - describe('systemWebhookUpdated', () => { - test('ActiveなWebhookが編集された時、キャッシュに反映されている', async () => { - const id = idService.gen(); - await createWebhook({ id }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていることをチェック - expect(webhook1.length).toEqual(1); - expect(webhook1[0].id).toEqual(id); - - const webhook2 = await service.updateSystemWebhook( - { - id, - isActive: true, - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks).toEqual([webhook2]); - }); - - test('NotActiveなWebhookが編集された時、キャッシュに追加されない', async () => { - const id = idService.gen(); - await createWebhook({ id, isActive: false }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていないことをチェック - expect(webhook1.length).toEqual(0); - - const webhook2 = await service.updateSystemWebhook( - { - id, - isActive: false, - name: randomString(), - on: ['abuseReport'], - url: 'https://example.com', - secret: randomString(), - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks.length).toEqual(0); - }); - - test('NotActiveなWebhookがActiveにされた時、キャッシュに追加されている', async () => { - const id = idService.gen(); - const baseWebhook = await createWebhook({ id, isActive: false }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていないことをチェック - expect(webhook1.length).toEqual(0); - - const webhook2 = await service.updateSystemWebhook( - { - ...baseWebhook, - isActive: true, - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks).toEqual([webhook2]); - }); - - test('ActiveなWebhookがNotActiveにされた時、キャッシュから削除されている', async () => { - const id = idService.gen(); - const baseWebhook = await createWebhook({ id, isActive: true }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていることをチェック - expect(webhook1.length).toEqual(1); - expect(webhook1[0].id).toEqual(id); - - const webhook2 = await service.updateSystemWebhook( - { - ...baseWebhook, - isActive: false, - }, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks.length).toEqual(0); - }); - }); - - describe('systemWebhookDeleted', () => { - test('キャッシュから削除されている', async () => { - const id = idService.gen(); - const baseWebhook = await createWebhook({ id, isActive: true }); - // キャッシュ作成 - const webhook1 = await service.fetchActiveSystemWebhooks(); - // 読み込まれていることをチェック - expect(webhook1.length).toEqual(1); - expect(webhook1[0].id).toEqual(id); - - const webhook2 = await service.deleteSystemWebhook( - id, - root, - ); - - // redisでの配信経由で更新されるのでちょっと待つ - await setTimeout(500); - - const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); - expect(fetchedWebhooks.length).toEqual(0); - }); - }); - }); - }); -}); diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts deleted file mode 100644 index 7ea325d420..0000000000 --- a/packages/backend/test/unit/UserSearchService.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import { describe, jest, test } from '@jest/globals'; -import { In } from 'typeorm'; -import { UserSearchService } from '@/core/UserSearchService.js'; -import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - -describe('UserSearchService', () => { - let app: TestingModule; - let service: UserSearchService; - - let usersRepository: UsersRepository; - let followingsRepository: FollowingsRepository; - let idService: IdService; - let userProfilesRepository: UserProfilesRepository; - - let root: MiUser; - let alice: MiUser; - let alyce: MiUser; - let alycia: MiUser; - let alysha: MiUser; - let alyson: MiUser; - let alyssa: MiUser; - let bob: MiUser; - let bobbi: MiUser; - let bobbie: MiUser; - let bobby: MiUser; - - async function createUser(data: Partial = {}) { - const user = await usersRepository - .insert({ - id: idService.gen(), - ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfilesRepository.insert({ - userId: user.id, - }); - - return user; - } - - async function createFollowings(follower: MiUser, followees: MiUser[]) { - for (const followee of followees) { - await followingsRepository.insert({ - id: idService.gen(), - followerId: follower.id, - followeeId: followee.id, - }); - } - } - - async function setActive(users: MiUser[]) { - for (const user of users) { - await usersRepository.update(user.id, { - updatedAt: new Date(), - }); - } - } - - async function setInactive(users: MiUser[]) { - for (const user of users) { - await usersRepository.update(user.id, { - updatedAt: new Date(0), - }); - } - } - - async function setSuspended(users: MiUser[]) { - for (const user of users) { - await usersRepository.update(user.id, { - isSuspended: true, - }); - } - } - - beforeAll(async () => { - app = await Test - .createTestingModule({ - imports: [ - GlobalModule, - ], - providers: [ - UserSearchService, - { - provide: UserEntityService, useFactory: jest.fn(() => ({ - // とりあえずIDが返れば確認が出来るので - packMany: (value: any) => value, - })), - }, - IdService, - ], - }) - .compile(); - - await app.init(); - - usersRepository = app.get(DI.usersRepository); - userProfilesRepository = app.get(DI.userProfilesRepository); - followingsRepository = app.get(DI.followingsRepository); - - service = app.get(UserSearchService); - idService = app.get(IdService); - }); - - beforeEach(async () => { - root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); - alice = await createUser({ username: 'Alice', usernameLower: 'alice' }); - alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' }); - alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' }); - alysha = await createUser({ username: 'Alysha', usernameLower: 'alysha' }); - alyson = await createUser({ username: 'Alyson', usernameLower: 'alyson', host: 'example.com' }); - alyssa = await createUser({ username: 'Alyssa', usernameLower: 'alyssa', host: 'example.com' }); - bob = await createUser({ username: 'Bob', usernameLower: 'bob' }); - bobbi = await createUser({ username: 'Bobbi', usernameLower: 'bobbi' }); - bobbie = await createUser({ username: 'Bobbie', usernameLower: 'bobbie', host: 'example.com' }); - bobby = await createUser({ username: 'Bobby', usernameLower: 'bobby', host: 'example.com' }); - }); - - afterEach(async () => { - await usersRepository.delete({}); - }); - - afterAll(async () => { - await app.close(); - }); - - describe('search', () => { - test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]); - await setInactive([alycia, alysha, alyson]); - - const result = await service.search( - { username: 'al' }, - { limit: 100 }, - root, - ); - - // alycia, alysha, alysonは非アクティブなので後ろに行く - expect(result).toEqual([alice, alyce, alyssa, alycia, alysha, alyson].map(x => x.id)); - }); - - test('フォロー中の非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - - const result = await service.search( - { username: 'al' }, - { limit: 100 }, - root, - ); - - // alice, alyceはフォローしていないので後ろに行く - expect(result).toEqual([alycia, alysha, alyson, alyssa, alice, alyce].map(x => x.id)); - }); - - test('フォローしていないアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setInactive([alice, alyce, alycia]); - - const result = await service.search( - { username: 'al' }, - { limit: 100 }, - root, - ); - - // alice, alyce, alyciaは非アクティブなので後ろに行く - expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id)); - }); - - test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - - const result = await service.search( - { username: 'al' }, - { limit: 100 }, - root, - ); - - expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id)); - }); - - test('フォロー(アクティブ)、フォロー(非アクティブ)、非フォロー(アクティブ)、非フォロー(非アクティブ)混在時の優先順位度確認', async () => { - await createFollowings(root, [alyson, alyssa, bob, bobbi, bobbie]); - await setActive([root, alyssa, bob, bobbi, alyce, alycia]); - await setInactive([alyson, alice, alysha, bobbie, bobby]); - - const result = await service.search( - { }, - { limit: 100 }, - root, - ); - - // 見る用 - // const users = await usersRepository.findBy({ id: In(result) }).then(it => new Map(it.map(x => [x.id, x]))); - // console.log(result.map(x => users.get(x as any)).map(it => it?.username)); - - // フォローしててアクティブなので先頭: alyssa, bob, bobbi - // フォローしてて非アクティブなので次: alyson, bobbie - // フォローしてないけどアクティブなので次: alyce, alycia, root(アルファベット順的にここになる) - // フォローしてないし非アクティブなので最後: alice, alysha, bobby - expect(result).toEqual([alyssa, bob, bobbi, alyson, bobbie, alyce, alycia, root, alice, alysha, bobby].map(x => x.id)); - }); - - test('[非ログイン] アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setInactive([alice, alyce, alycia]); - - const result = await service.search( - { username: 'al' }, - { limit: 100 }, - ); - - // alice, alyce, alyciaは非アクティブなので後ろに行く - expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id)); - }); - - test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => { - await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - - const result = await service.search( - { username: 'al' }, - { limit: 100 }, - ); - - expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id)); - }); - - test('フォロー中のアクティブユーザのうち、"al"から始まり"example.com"にいる人が全員ヒットする', async () => { - await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - - const result = await service.search( - { username: 'al', host: 'exam' }, - { limit: 100 }, - root, - ); - - expect(result).toEqual([alyson, alyssa].map(x => x.id)); - }); - - test('サスペンド済みユーザは出ない', async () => { - await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); - await setSuspended([alice, alyce, alycia]); - - const result = await service.search( - { username: 'al' }, - { limit: 100 }, - root, - ); - - expect(result).toEqual([alysha, alyson, alyssa].map(x => x.id)); - }); - }); -}); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 328417174f..fb403755f2 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,13 +13,11 @@ import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; -import { CONTEXT } from '@/core/activitypub/misc/contexts.js'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js'; +import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js'; import { MiMeta, MiNote } from '@/models/_.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DownloadService } from '@/core/DownloadService.js'; @@ -90,7 +88,6 @@ describe('ActivityPub', () => { let noteService: ApNoteService; let personService: ApPersonService; let rendererService: ApRendererService; - let jsonLdService: JsonLdService; let resolver: MockResolver; const metaInitial = { @@ -103,7 +100,6 @@ describe('ActivityPub', () => { perRemoteUserUserTimelineCacheMax: 100, blockedHosts: [] as string[], sensitiveWords: [] as string[], - prohibitedWords: [] as string[], } as MiMeta; let meta = metaInitial; @@ -131,7 +127,6 @@ describe('ActivityPub', () => { personService = app.get(ApPersonService); rendererService = app.get(ApRendererService); imageService = app.get(ApImageService); - jsonLdService = app.get(JsonLdService); resolver = new MockResolver(await app.resolve(LoggerService)); // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error @@ -207,7 +202,7 @@ describe('ActivityPub', () => { describe('Renderer', () => { test('Render an announce with visibility: followers', () => { - rendererService.renderAnnounce('https://example.com/notes/00example', { + rendererService.renderAnnounce(null, { id: genAidx(Date.now()), visibility: 'followers', } as MiNote); @@ -299,7 +294,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(driveFile && !driveFile.isLink); + assert.ok(!driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -312,7 +307,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), sensitiveImageObject, ); - assert.ok(sensitiveDriveFile && !sensitiveDriveFile.isLink); + assert.ok(!sensitiveDriveFile.isLink); }); test('cacheRemoteFiles=false disables caching', async () => { @@ -328,7 +323,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(driveFile && driveFile.isLink); + assert.ok(driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -341,7 +336,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), sensitiveImageObject, ); - assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); + assert.ok(sensitiveDriveFile.isLink); }); test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { @@ -357,7 +352,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), imageObject, ); - assert.ok(driveFile && !driveFile.isLink); + assert.ok(!driveFile.isLink); const sensitiveImageObject: IApDocument = { type: 'Document', @@ -370,57 +365,7 @@ describe('ActivityPub', () => { await createRandomRemoteUser(resolver, personService), sensitiveImageObject, ); - assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink); - }); - - test('Link is not an attachment files', async () => { - const linkObject: IObject = { - type: 'Link', - href: 'https://example.com/', - }; - const driveFile = await imageService.createImage( - await createRandomRemoteUser(resolver, personService), - linkObject, - ); - assert.strictEqual(driveFile, null); - }); - }); - - describe('JSON-LD', () => { - test('Compaction', async () => { - const jsonLd = jsonLdService.use(); - - const object = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - { - _misskey_quote: 'https://misskey-hub.net/ns#_misskey_quote', - unknown: 'https://example.org/ns#unknown', - undefined: null, - }, - ], - id: 'https://example.com/notes/42', - type: 'Note', - attributedTo: 'https://example.com/users/1', - to: ['https://www.w3.org/ns/activitystreams#Public'], - content: 'test test foo', - _misskey_quote: 'https://example.com/notes/1', - unknown: 'test test bar', - undefined: 'test test baz', - }; - const compacted = await jsonLd.compact(object); - - assert.deepStrictEqual(compacted, { - '@context': CONTEXT, - id: 'https://example.com/notes/42', - type: 'Note', - attributedTo: 'https://example.com/users/1', - to: 'as:Public', - content: 'test test foo', - _misskey_quote: 'https://example.com/notes/1', - 'https://example.org/ns#unknown': 'test test bar', - // undefined: 'test test baz', - }); + assert.ok(sensitiveDriveFile.isLink); }); }); }); diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index d3d39240dc..acfa9c271b 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts index 9dedd3a79d..8ec465cc24 100644 --- a/packages/backend/test/unit/chart.ts +++ b/packages/backend/test/unit/chart.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts deleted file mode 100644 index ee16d421c4..0000000000 --- a/packages/backend/test/unit/entities/UserEntityService.ts +++ /dev/null @@ -1,528 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { CoreModule } from '@/core/CoreModule.js'; -import type { MiUser } from '@/models/User.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { genAidx } from '@/misc/id/aidx.js'; -import { - BlockingsRepository, - FollowingsRepository, FollowRequestsRepository, - MiUserProfile, MutingsRepository, RenoteMutingsRepository, - UserMemoRepository, - UserProfilesRepository, - UsersRepository, -} from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { RoleService } from '@/core/RoleService.js'; -import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { IdService } from '@/core/IdService.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; -import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; -import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; -import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; -import { MfmService } from '@/core/MfmService.js'; -import { HashtagService } from '@/core/HashtagService.js'; -import UsersChart from '@/core/chart/charts/users.js'; -import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js'; -import InstanceChart from '@/core/chart/charts/instance.js'; -import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; -import { AccountMoveService } from '@/core/AccountMoveService.js'; -import { ReactionService } from '@/core/ReactionService.js'; -import { NotificationService } from '@/core/NotificationService.js'; - -process.env.NODE_ENV = 'test'; - -describe('UserEntityService', () => { - describe('pack/packMany', () => { - let app: TestingModule; - let service: UserEntityService; - let usersRepository: UsersRepository; - let userProfileRepository: UserProfilesRepository; - let userMemosRepository: UserMemoRepository; - let followingRepository: FollowingsRepository; - let followingRequestRepository: FollowRequestsRepository; - let blockingRepository: BlockingsRepository; - let mutingRepository: MutingsRepository; - let renoteMutingsRepository: RenoteMutingsRepository; - - async function createUser(userData: Partial = {}, profileData: Partial = {}) { - const un = secureRndstr(16); - const user = await usersRepository - .insert({ - ...userData, - id: genAidx(Date.now()), - username: un, - usernameLower: un, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - await userProfileRepository.insert({ - ...profileData, - userId: user.id, - }); - - return user; - } - - async function memo(writer: MiUser, target: MiUser, memo: string) { - await userMemosRepository.insert({ - id: genAidx(Date.now()), - userId: writer.id, - targetUserId: target.id, - memo, - }); - } - - async function follow(follower: MiUser, followee: MiUser) { - await followingRepository.insert({ - id: genAidx(Date.now()), - followerId: follower.id, - followeeId: followee.id, - }); - } - - async function requestFollow(requester: MiUser, requestee: MiUser) { - await followingRequestRepository.insert({ - id: genAidx(Date.now()), - followerId: requester.id, - followeeId: requestee.id, - }); - } - - async function block(blocker: MiUser, blockee: MiUser) { - await blockingRepository.insert({ - id: genAidx(Date.now()), - blockerId: blocker.id, - blockeeId: blockee.id, - }); - } - - async function mute(mutant: MiUser, mutee: MiUser) { - await mutingRepository.insert({ - id: genAidx(Date.now()), - muterId: mutant.id, - muteeId: mutee.id, - }); - } - - async function muteRenote(mutant: MiUser, mutee: MiUser) { - await renoteMutingsRepository.insert({ - id: genAidx(Date.now()), - muterId: mutant.id, - muteeId: mutee.id, - }); - } - - function randomIntRange(weight = 10) { - return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx); - } - - beforeAll(async () => { - const services = [ - UserEntityService, - ApPersonService, - NoteEntityService, - PageEntityService, - CustomEmojiService, - AnnouncementService, - RoleService, - FederatedInstanceService, - IdService, - AvatarDecorationService, - UtilityService, - EmojiEntityService, - ModerationLogService, - GlobalEventService, - DriveFileEntityService, - MetaService, - FetchInstanceMetadataService, - CacheService, - ApResolverService, - ApNoteService, - ApImageService, - ApMfmService, - MfmService, - HashtagService, - UsersChart, - ChartLoggerService, - InstanceChart, - ApLoggerService, - AccountMoveService, - ReactionService, - NotificationService, - ]; - - app = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule], - providers: [ - ...services, - ...services.map(x => ({ provide: x.name, useExisting: x })), - ], - }).compile(); - await app.init(); - app.enableShutdownHooks(); - - service = app.get(UserEntityService); - usersRepository = app.get(DI.usersRepository); - userProfileRepository = app.get(DI.userProfilesRepository); - userMemosRepository = app.get(DI.userMemosRepository); - followingRepository = app.get(DI.followingsRepository); - followingRequestRepository = app.get(DI.followRequestsRepository); - blockingRepository = app.get(DI.blockingsRepository); - mutingRepository = app.get(DI.mutingsRepository); - renoteMutingsRepository = app.get(DI.renoteMutingsRepository); - }); - - afterAll(async () => { - await app.close(); - }); - - test('UserLite', async() => { - const me = await createUser(); - const who = await createUser(); - - await memo(me, who, 'memo'); - - const actual = await service.pack(who, me, { schema: 'UserLite' }) as any; - // no detail - expect(actual.memo).toBeUndefined(); - // no detail and me - expect(actual.birthday).toBeUndefined(); - // no detail and me - expect(actual.achievements).toBeUndefined(); - }); - - test('UserDetailedNotMe', async() => { - const me = await createUser(); - const who = await createUser({}, { birthday: '2000-01-01' }); - - await memo(me, who, 'memo'); - - const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any; - // is detail - expect(actual.memo).toBe('memo'); - // is detail - expect(actual.birthday).toBe('2000-01-01'); - // no detail and me - expect(actual.achievements).toBeUndefined(); - }); - - test('MeDetailed', async() => { - const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }]; - const me = await createUser({}, { - birthday: '2000-01-01', - achievements: achievements, - }); - await memo(me, me, 'memo'); - - const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any; - // is detail - expect(actual.memo).toBe('memo'); - // is detail - expect(actual.birthday).toBe('2000-01-01'); - // is detail and me - expect(actual.achievements).toEqual(achievements); - }); - - describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => { - test('no-preload', async() => { - const me = await createUser(); - // meがフォローしてる人たち - const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followeeMe) { - await follow(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(true); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meをフォローしてる人たち - const followerMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followerMe) { - await follow(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(true); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがフォローリクエストを送った人たち - const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsFromYou) { - await requestFollow(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(true); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meにフォローリクエストを送った人たち - const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsToYou) { - await requestFollow(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(true); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがブロックしてる人たち - const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingYou) { - await block(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(true); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meをブロックしてる人たち - const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingMe) { - await block(who, me); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(true); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがミュートしてる人たち - const muters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of muters) { - await mute(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(true); - expect(actual.isRenoteMuted).toBe(false); - } - - // meがリノートミュートしてる人たち - const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of renoteMuters) { - await muteRenote(me, who); - const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(true); - } - }); - - test('preload', async() => { - const me = await createUser(); - - { - // meがフォローしてる人たち - const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followeeMe) { - await follow(me, who); - } - const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(true); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meをフォローしてる人たち - const followerMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of followerMe) { - await follow(who, me); - } - const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(true); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがフォローリクエストを送った人たち - const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsFromYou) { - await requestFollow(me, who); - } - const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(true); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meにフォローリクエストを送った人たち - const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of requestsToYou) { - await requestFollow(who, me); - } - const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(true); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがブロックしてる人たち - const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingYou) { - await block(me, who); - } - const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(true); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meをブロックしてる人たち - const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of blockingMe) { - await block(who, me); - } - const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(true); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがミュートしてる人たち - const muters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of muters) { - await mute(me, who); - } - const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(true); - expect(actual.isRenoteMuted).toBe(false); - } - } - - { - // meがリノートミュートしてる人たち - const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); - for (const who of renoteMuters) { - await muteRenote(me, who); - } - const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any; - for (const actual of actualList) { - expect(actual.isFollowing).toBe(false); - expect(actual.isFollowed).toBe(false); - expect(actual.hasPendingFollowRequestFromYou).toBe(false); - expect(actual.hasPendingFollowRequestToYou).toBe(false); - expect(actual.isBlocking).toBe(false); - expect(actual.isBlocked).toBe(false); - expect(actual.isMuted).toBe(false); - expect(actual.isRenoteMuted).toBe(true); - } - } - }); - }); - }); -}); diff --git a/packages/backend/test/unit/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts index bd9d818565..6b6b97a267 100644 --- a/packages/backend/test/unit/extract-mentions.ts +++ b/packages/backend/test/unit/extract-mentions.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/test/unit/misc/check-word-mute.ts b/packages/backend/test/unit/misc/check-word-mute.ts index eb0ca0f6cf..3fcfb0baff 100644 --- a/packages/backend/test/unit/misc/check-word-mute.ts +++ b/packages/backend/test/unit/misc/check-word-mute.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/test/unit/misc/correct-filename.ts b/packages/backend/test/unit/misc/correct-filename.ts index c76fb4c494..06fdbc1d2a 100644 --- a/packages/backend/test/unit/misc/correct-filename.ts +++ b/packages/backend/test/unit/misc/correct-filename.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/backend/test/unit/misc/id.ts b/packages/backend/test/unit/misc/id.ts index d14efb10a6..090429ac3c 100644 --- a/packages/backend/test/unit/misc/id.ts +++ b/packages/backend/test/unit/misc/id.ts @@ -1,16 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { ulid } from 'ulid'; -import { describe, expect, test } from '@jest/globals'; +import { describe, test, expect } from '@jest/globals'; import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js'; import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js'; import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js'; import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js'; import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.js'; -import { parseUlid, ulidRegExp } from '@/misc/id/ulid.js'; +import { ulidRegExp, parseUlid } from '@/misc/id/ulid.js'; describe('misc:id', () => { test('aid', () => { diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts deleted file mode 100644 index 0b713e8bf6..0000000000 --- a/packages/backend/test/unit/misc/is-renote.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { isQuote, isRenote } from '@/misc/is-renote.js'; -import { MiNote } from '@/models/Note.js'; - -const base: MiNote = { - id: 'some-note-id', - replyId: null, - reply: null, - renoteId: null, - renote: null, - threadId: null, - text: null, - name: null, - cw: null, - userId: 'some-user-id', - user: null, - localOnly: false, - reactionAcceptance: null, - renoteCount: 0, - repliesCount: 0, - clippedCount: 0, - reactions: {}, - visibility: 'public', - uri: null, - url: null, - fileIds: [], - attachedFileTypes: [], - visibleUserIds: [], - mentions: [], - mentionedRemoteUsers: '', - reactionAndUserPairCache: [], - emojis: [], - tags: [], - hasPoll: false, - channelId: null, - channel: null, - userHost: null, - replyUserId: null, - replyUserHost: null, - renoteUserId: null, - renoteUserHost: null, -}; - -describe('misc:is-renote', () => { - test('note without renoteId should not be Renote', () => { - expect(isRenote(base)).toBe(false); - }); - - test('note with renoteId should be Renote and not be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id' }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(false); - }); - - test('note with renoteId and text should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', text: 'some-text' }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); - - test('note with renoteId and cw should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', cw: 'some-cw' }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); - - test('note with renoteId and replyId should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', replyId: 'some-reply-id' }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); - - test('note with renoteId and poll should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', hasPoll: true }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); - - test('note with renoteId and non-empty fileIds should be Quote', () => { - const note: MiNote = { ...base, renoteId: 'some-renote-id', fileIds: ['some-file-id'] }; - expect(isRenote(note)).toBe(true); - expect(isQuote(note as any)).toBe(true); - }); -}); diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts index 2cf54e1555..fa37950951 100644 --- a/packages/backend/test/unit/misc/loader.ts +++ b/packages/backend/test/unit/misc/loader.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { DebounceLoader } from '@/misc/loader.js'; class Mock { diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts index 3bc134a2b8..6182590233 100644 --- a/packages/backend/test/unit/misc/others.ts +++ b/packages/backend/test/unit/misc/others.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { describe, expect, test } from '@jest/globals'; +import { describe, test, expect } from '@jest/globals'; import { contentDisposition } from '@/misc/content-disposition.js'; describe('misc:content-disposition', () => { diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 87b090de70..f000aa7bb4 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,90 +1,74 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; -import { basename, isAbsolute } from 'node:path'; +import { isAbsolute, basename } from 'node:path'; import { randomUUID } from 'node:crypto'; import { inspect } from 'node:util'; import WebSocket, { ClientOptions } from 'ws'; -import fetch, { File, RequestInit, type Headers } from 'node-fetch'; +import fetch, { File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; import { JSDOM } from 'jsdom'; -import { type Response } from 'node-fetch'; -import Fastify from 'fastify'; -import { entities } from '../src/postgres.js'; -import { loadConfig } from '../src/config.js'; -import type * as misskey from 'cherrypick-js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import { ApiError } from '@/server/api/error.js'; +import { entities } from '@/postgres.js'; +import { loadConfig } from '@/config.js'; +import type * as misskey from 'cherrypick-js'; -export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; +export { server as startServer } from '@/boot/common.js'; -export interface UserToken { +interface UserToken { token: string; bearer?: boolean; } -export type SystemWebhookPayload = { - server: string; - hookId: string; - eventId: string; - createdAt: string; - type: string; - body: any; -} - const config = loadConfig(); export const port = config.port; export const origin = config.url; export const host = new URL(config.url).host; -export const WEBHOOK_HOST = 'http://localhost:15080'; -export const WEBHOOK_PORT = 15080; - export const cookie = (me: UserToken): string => { return `token=${me.token};`; }; -export type ApiRequest = { - endpoint: E, - parameters: P, +export const api = async (endpoint: string, params: any, me?: UserToken) => { + const normalized = endpoint.replace(/^\//, ''); + return await request(`api/${normalized}`, params, me); +}; + +export type ApiRequest = { + endpoint: string, + parameters: object, user: UserToken | undefined, }; -export const successfulApiCall = async (request: ApiRequest, assertion: { +export const successfulApiCall = async (request: ApiRequest, assertion: { status?: number, -} = {}): Promise> => { +} = {}): Promise => { const { endpoint, parameters, user } = request; const res = await api(endpoint, parameters, user); const status = assertion.status ?? (res.body == null ? 204 : 200); assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true })); - - return res.body as misskey.api.SwitchCaseResponseType; + return res.body; }; -export const failedApiCall = async (request: ApiRequest, assertion: { +export const failedApiCall = async (request: ApiRequest, assertion: { status: number, code: string, id: string -}): Promise => { +}): Promise => { const { endpoint, parameters, user } = request; const { status, code, id } = assertion; const res = await api(endpoint, parameters, user); assert.strictEqual(res.status, status, inspect(res.body)); - assert.ok(res.body); - assert.strictEqual(castAsError(res.body as any).error.code, code, inspect(res.body)); - assert.strictEqual(castAsError(res.body as any).error.id, id, inspect(res.body)); + assert.strictEqual(res.body.error.code, code, inspect(res.body)); + assert.strictEqual(res.body.error.id, id, inspect(res.body)); + return res.body; }; -export const api = async (path: E, params: P, me?: UserToken): Promise<{ - status: number, - headers: Headers, - body: misskey.api.SwitchCaseResponseType -}> => { +const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => { const bodyAuth: Record = {}; const headers: Record = { 'Content-Type': 'application/json', @@ -96,7 +80,7 @@ export const api = async + ? await res.json() : null; return { status: res.status, headers: res.headers, - // FIXME: removing this non-null assertion: requires better typing around empty response. - body: body!, + body, }; }; @@ -127,20 +110,6 @@ export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', len return randomString; } -/** - * @brief プロミスにタイムアウト追加 - * @param p 待ち対象プロミス - * @param timeout 待機ミリ秒 - */ -function timeoutPromise(p: Promise, timeout: number): Promise { - return Promise.race([ - p, - new Promise((reject) => { - setTimeout(() => { reject(new Error('timed out')); }, timeout); - }) as never, - ]); -} - export const signup = async (params?: Partial): Promise> => { const q = Object.assign({ username: randomString(), @@ -152,13 +121,12 @@ export const signup = async (params?: Partial => { +export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise => { const q = params; const res = await api('notes/create', q, user); - // FIXME: the return type should reflect this fact. - return (res.body ? res.body.createdNote : null)!; + return res.body ? res.body.createdNote : null; }; export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => { @@ -171,8 +139,8 @@ export const createAppToken = async (user: UserToken, permissions: (typeof missk }; // 非公開ノートをAPI越しに見たときのノート NoteEntityService.ts -export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note => { - const temp: misskey.entities.Note = { +export const hiddenNote = (note: any): any => { + const temp = { ...note, fileIds: [], files: [], @@ -185,22 +153,21 @@ export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note = return temp; }; -export const react = async (user: UserToken, note: misskey.entities.Note, reaction: string): Promise => { +export const react = async (user: UserToken, note: any, reaction: string): Promise => { await api('notes/reactions/create', { noteId: note.id, reaction: reaction, }, user); }; -export const userList = async (user: UserToken, userList: Partial = {}): Promise => { +export const userList = async (user: UserToken, userList: any = {}): Promise => { const res = await api('users/lists/create', { name: 'test', - ...userList, }, user); return res.body; }; -export const page = async (user: UserToken, page: Partial = {}): Promise => { +export const page = async (user: UserToken, page: any = {}): Promise => { const res = await api('pages/create', { alignCenter: false, content: [ @@ -211,7 +178,7 @@ export const page = async (user: UserToken, page: Partial }, ], eyeCatchingImageId: null, - font: 'sans-serif' as any, + font: 'sans-serif', hideTitleWhenPinned: false, name: '1678594845072', script: '', @@ -223,7 +190,7 @@ export const page = async (user: UserToken, page: Partial return res.body; }; -export const play = async (user: UserToken, play: Partial = {}): Promise => { +export const play = async (user: UserToken, play: any = {}): Promise => { const res = await api('flash/create', { permissions: [], script: 'test', @@ -234,7 +201,7 @@ export const play = async (user: UserToken, play: Partial = {}): Promise => { +export const clip = async (user: UserToken, clip: any = {}): Promise => { const res = await api('clips/create', { description: null, isPublic: true, @@ -244,18 +211,18 @@ export const clip = async (user: UserToken, clip: Partial return res.body; }; -export const galleryPost = async (user: UserToken, galleryPost: Partial = {}): Promise => { +export const galleryPost = async (user: UserToken, channel: any = {}): Promise => { const res = await api('gallery/posts/create', { description: null, fileIds: [], isSensitive: false, title: 'test', - ...galleryPost, + ...channel, }, user); return res.body; }; -export const channel = async (user: UserToken, channel: Partial = {}): Promise => { +export const channel = async (user: UserToken, channel: any = {}): Promise => { const res = await api('channels/create', { bannerId: null, description: null, @@ -265,7 +232,7 @@ export const channel = async (user: UserToken, channel: Partial = {}, policies: any = {}): Promise => { +export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise => { const res = await api('admin/roles/create', { asBadge: false, canEditMembersByModerator: false, @@ -273,7 +240,7 @@ export const role = async (user: UserToken, role: Partial condFormula: { id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85', type: 'isRemote', - } as any, + }, description: '', displayOrder: 0, iconUrl: null, @@ -308,13 +275,9 @@ interface UploadOptions { * Upload file * @param user User */ -export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ - status: number, - headers: Headers, - body: misskey.entities.DriveFile | null -}> => { +export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => { const absPath = path == null - ? new URL('resources/192.jpg', import.meta.url) + ? new URL('resources/Lenna.jpg', import.meta.url) : isAbsolute(path.toString()) ? new URL(path) : new URL(path, new URL('resources/', import.meta.url)); @@ -341,6 +304,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO }); const body = res.status !== 204 ? await res.json() as misskey.Endpoints['drive/files/create']['res'] : null; + return { status: res.status, headers: res.headers, @@ -348,16 +312,17 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO }; }; -export const uploadUrl = async (user: UserToken, url: string): Promise => { +export const uploadUrl = async (user: UserToken, url: string) => { + let resolve: unknown; + const file = new Promise(ok => resolve = ok); const marker = Math.random().toString(); - const catcher = makeStreamCatcher( - user, - 'main', - (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, - (msg) => msg.body.file, - 60 * 1000, - ); + const ws = await connectStream(user, 'main', (msg) => { + if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) { + ws.close(); + resolve(msg.body.file); + } + }); await api('drive/files/upload-from-url', { url, @@ -365,10 +330,10 @@ export const uploadUrl = async (user: UserToken, url: string): Promise(user: UserToken, channel: C, listener: (message: Record) => any, params?: misskey.Channels[C]['params']): Promise { +export function connectStream(user: UserToken, channel: string, listener: (message: Record) => any, params?: any): Promise { return new Promise((res, rej) => { const url = new URL(`ws://127.0.0.1:${port}/streaming`); const options: ClientOptions = {}; @@ -403,7 +368,7 @@ export function connectStream(user: UserToken, }); } -export const waitFire = async (user: UserToken, channel: C, trgr: () => any, cond: (msg: Record) => boolean, params?: misskey.Channels[C]['params']) => { +export const waitFire = async (user: UserToken, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { return new Promise(async (res, rej) => { let timer: NodeJS.Timeout | null = null; @@ -437,42 +402,13 @@ export const waitFire = async (user: UserToken }); }; -/** - * @brief WebSocketストリームから特定条件の通知を拾うプロミスを生成 - * @param user ユーザー認証情報 - * @param channel チャンネル - * @param cond 条件 - * @param extractor 取り出し処理 - * @param timeout ミリ秒タイムアウト - * @returns 時間内に正常に処理できた場合に通知からextractorを通した値を得る - */ -export function makeStreamCatcher( - user: UserToken, - channel: keyof misskey.Channels, - cond: (message: Record) => boolean, - extractor: (message: Record) => T, - timeout = 60 * 1000): Promise { - let ws: WebSocket; - const p = new Promise(async (resolve) => { - ws = await connectStream(user, channel, (msg) => { - if (cond(msg)) { - resolve(extractor(msg)); - } - }); - }).finally(() => { - ws.close(); - }); - - return timeoutPromise(p, timeout); -} - export type SimpleGetResponse = { status: number, body: any | JSDOM | null, type: string | null, location: string | null }; -export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined, bodyExtractor: (res: Response) => Promise = _ => Promise.resolve(null)): Promise => { +export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise => { const res = await relativeFetch(path, { headers: { Accept: accept, @@ -489,18 +425,10 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde 'text/html; charset=utf-8', ]; - if (res.ok && ( - accept.startsWith('application/activity+json') || - (accept.startsWith('application/ld+json') && accept.includes('https://www.w3.org/ns/activitystreams')) - )) { - // validateContentTypeSetAsActivityPubのテストを兼ねる - validateContentTypeSetAsActivityPub(res); - } - const body = - jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : - htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : - await bodyExtractor(res); + jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : + htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : + null; return { status: res.status, @@ -622,73 +550,10 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { return db; } -export async function sendEnvUpdateRequest(params: { key: string, value?: string }) { - const res = await fetch( - `http://localhost:${port + 1000}/env`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(params), - }, - ); - - if (res.status !== 200) { - throw new Error('server env update failed.'); - } -} - -export async function sendEnvResetRequest() { - const res = await fetch( - `http://localhost:${port + 1000}/env-reset`, - { - method: 'POST', - body: JSON.stringify({}), - }, - ); - - if (res.status !== 200) { - throw new Error('server env update failed.'); - } -} - -// 与えられた値を強制的にエラーとみなす。この関数は型安全性を破壊するため、異常系のアサーション以外で用いられるべきではない。 -// FIXME(cherrypick-js): cherrypick-jsがエラー情報を公開するようになったらこの関数を廃止する -export function castAsError(obj: Record): { error: ApiError } { - return obj as { error: ApiError }; -} - -export async function captureWebhook(postAction: () => Promise, port = WEBHOOK_PORT): Promise { - const fastify = Fastify(); - - let timeoutHandle: NodeJS.Timeout | null = null; - const result = await new Promise(async (resolve, reject) => { - fastify.all('/', async (req, res) => { - timeoutHandle && clearTimeout(timeoutHandle); - - const body = JSON.stringify(req.body); - res.status(200).send('ok'); - await fastify.close(); - resolve(body); - }); - - await fastify.listen({ port }); - - timeoutHandle = setTimeout(async () => { - await fastify.close(); - reject(new Error('timeout')); - }, 3000); - - try { - await postAction(); - } catch (e) { - await fastify.close(); - reject(e); - } +export function sleep(msec: number) { + return new Promise(res => { + setTimeout(() => { + res(); + }, msec); }); - - await fastify.close(); - - return JSON.parse(result) as T; } diff --git a/packages/backend/scripts/watch.mjs b/packages/backend/watch.mjs similarity index 87% rename from packages/backend/scripts/watch.mjs rename to packages/backend/watch.mjs index a0ccea3b16..9413129bb4 100644 --- a/packages/backend/scripts/watch.mjs +++ b/packages/backend/watch.mjs @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/cherrypick-js/.eslintignore b/packages/cherrypick-js/.eslintignore new file mode 100644 index 0000000000..f22128f047 --- /dev/null +++ b/packages/cherrypick-js/.eslintignore @@ -0,0 +1,7 @@ +node_modules +/built +/coverage +/.eslintrc.js +/jest.config.ts +/test +/test-d diff --git a/packages/cherrypick-js/.eslintrc.cjs b/packages/cherrypick-js/.eslintrc.cjs new file mode 100644 index 0000000000..e2e31e9e33 --- /dev/null +++ b/packages/cherrypick-js/.eslintrc.cjs @@ -0,0 +1,9 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + extends: [ + '../shared/.eslintrc.js', + ], +}; diff --git a/packages/cherrypick-js/LICENSE b/packages/cherrypick-js/LICENSE index e24ce71ce9..11c1f9ce22 100644 --- a/packages/cherrypick-js/LICENSE +++ b/packages/cherrypick-js/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2024 syuilo and noridev and other contributors +Copyright (c) 2021-2022 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/cherrypick-js/README.md b/packages/cherrypick-js/README.md index 5f3eed9436..6526a318af 100644 --- a/packages/cherrypick-js/README.md +++ b/packages/cherrypick-js/README.md @@ -154,5 +154,5 @@ stream.on('_disconnected_', () => { ---
- +
diff --git a/packages/cherrypick-js/biome.json b/packages/cherrypick-js/biome.json deleted file mode 100644 index 1c160db391..0000000000 --- a/packages/cherrypick-js/biome.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", - "organizeImports": { "enabled": true }, - "linter": { - "enabled": true, - "rules": { - "recommended": false, - "complexity": { - "noBannedTypes": "error", - "noExtraBooleanCast": "error", - "noMultipleSpacesInRegularExpressionLiterals": "error", - "noUselessCatch": "error", - "noUselessTypeConstraint": "error", - "noWith": "error" - }, - "correctness": { - "noConstAssign": "error", - "noConstantCondition": "warn", - "noEmptyCharacterClassInRegex": "error", - "noEmptyPattern": "warn", - "noGlobalObjectCalls": "error", - "noInnerDeclarations": "off", - "noInvalidConstructorSuper": "error", - "noNewSymbol": "error", - "noNonoctalDecimalEscape": "error", - "noPrecisionLoss": "error", - "noSelfAssign": "error", - "noSetterReturn": "error", - "noSwitchDeclarations": "error", - "noUndeclaredVariables": "error", - "noUnreachable": "error", - "noUnreachableSuper": "error", - "noUnsafeFinally": "error", - "noUnsafeOptionalChaining": "error", - "noUnusedLabels": "error", - "noUnusedVariables": "error", - "useArrayLiterals": "off", - "useIsNan": "error", - "useValidForDirection": "error", - "useYield": "error" - }, - "style": { - "noDefaultExport": "warn", - "noInferrableTypes": "warn", - "noNamespace": "error", - "noNonNullAssertion": "warn", - "noParameterAssign": "warn", - "noVar": "error", - "useAsConstAssertion": "error" - }, - "suspicious": { - "noAsyncPromiseExecutor": "off", - "noCatchAssign": "error", - "noClassAssign": "error", - "noCompareNegZero": "error", - "noControlCharactersInRegex": "warn", - "noDebugger": "error", - "noDoubleEquals": "error", - "noDuplicateCase": "error", - "noDuplicateClassMembers": "error", - "noDuplicateObjectKeys": "error", - "noDuplicateParameters": "error", - "noEmptyBlockStatements": "off", - "noExplicitAny": "warn", - "noExtraNonNullAssertion": "error", - "noFallthroughSwitchClause": "error", - "noFunctionAssign": "error", - "noGlobalAssign": "error", - "noImportAssign": "error", - "noMisleadingCharacterClass": "error", - "noMisleadingInstantiator": "error", - "noPrototypeBuiltins": "error", - "noRedeclare": "error", - "noShadowRestrictedNames": "error", - "noUnsafeDeclarationMerging": "error", - "noUnsafeNegation": "error", - "useGetterReturn": "error", - "useValidTypeof": "error" - } - }, - "ignore": [ - "**/.eslintrc.cjs", - "**/node_modules", - "./built", - "./coverage", - "./.eslintrc.js", - "./jest.config.ts", - "./test", - "./test-d", - "**/build.js" - ] - }, - "overrides": [ - { - "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], - "linter": { - "rules": { - "correctness": { - "noConstAssign": "off", - "noGlobalObjectCalls": "off", - "noInvalidConstructorSuper": "off", - "noInvalidNewBuiltin": "off", - "noNewSymbol": "off", - "noSetterReturn": "off", - "noUndeclaredVariables": "off", - "noUnreachable": "off", - "noUnreachableSuper": "off" - }, - "style": { - "noArguments": "error", - "noVar": "error", - "useConst": "error" - }, - "suspicious": { - "noDuplicateClassMembers": "off", - "noDuplicateObjectKeys": "off", - "noDuplicateParameters": "off", - "noFunctionAssign": "off", - "noImportAssign": "off", - "noRedeclare": "off", - "noUnsafeNegation": "off", - "useGetterReturn": "off" - } - } - } - } - ] -} diff --git a/packages/cherrypick-js/build.js b/packages/cherrypick-js/build.js deleted file mode 100644 index a80b71646f..0000000000 --- a/packages/cherrypick-js/build.js +++ /dev/null @@ -1,104 +0,0 @@ -import fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import * as esbuild from 'esbuild'; -import { build } from 'esbuild'; -import { globSync } from 'glob'; -import { execa } from 'execa'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); -const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); - -const entryPoints = globSync('./src/**/**.{ts,tsx}'); - -/** @type {import('esbuild').BuildOptions} */ -const options = { - entryPoints, - minify: process.env.NODE_ENV === 'production', - outdir: './built', - target: 'es2022', - platform: 'browser', - format: 'esm', - sourcemap: 'linked', -}; - -// built配下をすべて削除する -fs.rmSync('./built', { recursive: true, force: true }); - -if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { - await watchSrc(); -} else { - await buildSrc(); -} - -async function buildSrc() { - console.log(`[${_package.name}] start building...`); - - await build(options) - .then(() => { - console.log(`[${_package.name}] build succeeded.`); - }) - .catch((err) => { - process.stderr.write(err.stderr); - process.exit(1); - }); - - if (process.env.NODE_ENV === 'production') { - console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`); - } else { - await buildDts(); - } - - console.log(`[${_package.name}] finish building.`); -} - -function buildDts() { - return execa( - 'tsc', - [ - '--project', 'tsconfig.json', - '--outDir', 'built', - '--declaration', 'true', - '--emitDeclarationOnly', 'true', - ], - { - stdout: process.stdout, - stderr: process.stderr, - }, - ); -} - -async function watchSrc() { - const plugins = [{ - name: 'gen-dts', - setup(build) { - build.onStart(() => { - console.log(`[${_package.name}] detect changed...`); - }); - build.onEnd(async result => { - if (result.errors.length > 0) { - console.error(`[${_package.name}] watch build failed:`, result); - return; - } - await buildDts(); - }); - }, - }]; - - console.log(`[${_package.name}] start watching...`); - - const context = await esbuild.context({ ...options, plugins }); - await context.watch(); - - await new Promise((resolve, reject) => { - process.on('SIGHUP', resolve); - process.on('SIGINT', resolve); - process.on('SIGTERM', resolve); - process.on('uncaughtException', reject); - process.on('exit', resolve); - }).finally(async () => { - await context.dispose(); - console.log(`[${_package.name}] finish watching.`); - }); -} diff --git a/packages/cherrypick-js/eslint.config.js b/packages/cherrypick-js/eslint.config.js deleted file mode 100644 index d8173f30e9..0000000000 --- a/packages/cherrypick-js/eslint.config.js +++ /dev/null @@ -1,29 +0,0 @@ -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../shared/eslint.config.js'; - -// eslint-disable-next-line import/no-default-export -export default [ - ...sharedConfig, - { - ignores: [ - '**/node_modules', - 'built', - 'coverage', - 'jest.config.ts', - 'test', - 'test-d', - 'generator', - ], - }, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - }, -]; diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 61eb969a58..bd5513d36b 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -6,11 +6,6 @@ import { EventEmitter } from 'eventemitter3'; -// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient']; - // @public (undocumented) export type Acct = { username: string; @@ -26,394 +21,333 @@ declare namespace acct { } export { acct } +// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts +// // @public (undocumented) type Ad = components['schemas']['Ad']; // Warning: (ae-forgotten-export) The symbol "operations" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json']; +type AdminAbuseReportResolverCreateRequest = operations['admin/abuse-report-resolver/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json']; +type AdminAbuseReportResolverCreateResponse = operations['admin/abuse-report-resolver/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json']; +type AdminAbuseReportResolverDeleteRequest = operations['admin/abuse-report-resolver/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json']; +type AdminAbuseReportResolverListRequest = operations['admin/abuse-report-resolver/list']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json']; +type AdminAbuseReportResolverListResponse = operations['admin/abuse-report-resolver/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json']; +type AdminAbuseReportResolverUpdateRequest = operations['admin/abuse-report-resolver/update']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json']; +type AdminAbuseUserReportsRequest = operations['admin/abuse-user-reports']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json']; +type AdminAbuseUserReportsResponse = operations['admin/abuse-user-reports']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportResolverCreateRequest = operations['admin___abuse-report-resolver___create']['requestBody']['content']['application/json']; +type AdminAccountsCreateRequest = operations['admin/accounts/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportResolverCreateResponse = operations['admin___abuse-report-resolver___create']['responses']['200']['content']['application/json']; +type AdminAccountsCreateResponse = operations['admin/accounts/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportResolverDeleteRequest = operations['admin___abuse-report-resolver___delete']['requestBody']['content']['application/json']; +type AdminAccountsDeleteRequest = operations['admin/accounts/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportResolverListRequest = operations['admin___abuse-report-resolver___list']['requestBody']['content']['application/json']; +type AdminAccountsFindByEmailRequest = operations['admin/accounts/find-by-email']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportResolverListResponse = operations['admin___abuse-report-resolver___list']['responses']['200']['content']['application/json']; +type AdminAccountsFindByEmailResponse = operations['admin/accounts/find-by-email']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAbuseReportResolverUpdateRequest = operations['admin___abuse-report-resolver___update']['requestBody']['content']['application/json']; +type AdminAdCreateRequest = operations['admin/ad/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json']; +type AdminAdCreateResponse = operations['admin/ad/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAbuseUserReportsResponse = operations['admin___abuse-user-reports']['responses']['200']['content']['application/json']; +type AdminAdDeleteRequest = operations['admin/ad/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAccountsCreateRequest = operations['admin___accounts___create']['requestBody']['content']['application/json']; +type AdminAdListRequest = operations['admin/ad/list']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAccountsCreateResponse = operations['admin___accounts___create']['responses']['200']['content']['application/json']; +type AdminAdListResponse = operations['admin/ad/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAccountsDeleteRequest = operations['admin___accounts___delete']['requestBody']['content']['application/json']; +type AdminAdUpdateRequest = operations['admin/ad/update']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAccountsFindByEmailRequest = operations['admin___accounts___find-by-email']['requestBody']['content']['application/json']; +type AdminAnnouncementsCreateRequest = operations['admin/announcements/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAccountsFindByEmailResponse = operations['admin___accounts___find-by-email']['responses']['200']['content']['application/json']; +type AdminAnnouncementsCreateResponse = operations['admin/announcements/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAdCreateRequest = operations['admin___ad___create']['requestBody']['content']['application/json']; +type AdminAnnouncementsDeleteRequest = operations['admin/announcements/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAdCreateResponse = operations['admin___ad___create']['responses']['200']['content']['application/json']; +type AdminAnnouncementsListRequest = operations['admin/announcements/list']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAdDeleteRequest = operations['admin___ad___delete']['requestBody']['content']['application/json']; +type AdminAnnouncementsListResponse = operations['admin/announcements/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAdListRequest = operations['admin___ad___list']['requestBody']['content']['application/json']; +type AdminAnnouncementsUpdateRequest = operations['admin/announcements/update']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAdListResponse = operations['admin___ad___list']['responses']['200']['content']['application/json']; +type AdminAvatarDecorationsCreateRequest = operations['admin/avatar-decorations/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAdUpdateRequest = operations['admin___ad___update']['requestBody']['content']['application/json']; +type AdminAvatarDecorationsDeleteRequest = operations['admin/avatar-decorations/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAnnouncementsCreateRequest = operations['admin___announcements___create']['requestBody']['content']['application/json']; +type AdminAvatarDecorationsListRequest = operations['admin/avatar-decorations/list']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAnnouncementsCreateResponse = operations['admin___announcements___create']['responses']['200']['content']['application/json']; +type AdminAvatarDecorationsListResponse = operations['admin/avatar-decorations/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAnnouncementsDeleteRequest = operations['admin___announcements___delete']['requestBody']['content']['application/json']; +type AdminAvatarDecorationsUpdateRequest = operations['admin/avatar-decorations/update']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAnnouncementsListRequest = operations['admin___announcements___list']['requestBody']['content']['application/json']; +type AdminDeleteAccountRequest = operations['admin/delete-account']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAnnouncementsListResponse = operations['admin___announcements___list']['responses']['200']['content']['application/json']; +type AdminDeleteAccountResponse = operations['admin/delete-account']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json']; +type AdminDeleteAllFilesOfAUserRequest = operations['admin/delete-all-files-of-a-user']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json']; +type AdminDriveFilesRequest = operations['admin/drive/files']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json']; +type AdminDriveFilesResponse = operations['admin/drive/files']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json']; +type AdminDriveShowFileRequest = operations['admin/drive/show-file']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json']; +type AdminDriveShowFileResponse = operations['admin/drive/show-file']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json']; +type AdminEmojiAddAliasesBulkRequest = operations['admin/emoji/add-aliases-bulk']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json']; +type AdminEmojiAddRequest = operations['admin/emoji/add']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminDeleteAllFilesOfAUserRequest = operations['admin___delete-all-files-of-a-user']['requestBody']['content']['application/json']; +type AdminEmojiAddsRequest = operations['admin/emoji/adds']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminDriveFilesRequest = operations['admin___drive___files']['requestBody']['content']['application/json']; +type AdminEmojiCopyRequest = operations['admin/emoji/copy']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminDriveFilesResponse = operations['admin___drive___files']['responses']['200']['content']['application/json']; +type AdminEmojiCopyResponse = operations['admin/emoji/copy']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminDriveShowFileRequest = operations['admin___drive___show-file']['requestBody']['content']['application/json']; +type AdminEmojiDeleteBulkRequest = operations['admin/emoji/delete-bulk']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminDriveShowFileResponse = operations['admin___drive___show-file']['responses']['200']['content']['application/json']; +type AdminEmojiDeleteRequest = operations['admin/emoji/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiAddAliasesBulkRequest = operations['admin___emoji___add-aliases-bulk']['requestBody']['content']['application/json']; +type AdminEmojiImportZipRequest = operations['admin/emoji/import-zip']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiAddRequest = operations['admin___emoji___add']['requestBody']['content']['application/json']; +type AdminEmojiListRemoteRequest = operations['admin/emoji/list-remote']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiAddResponse = operations['admin___emoji___add']['responses']['200']['content']['application/json']; +type AdminEmojiListRemoteResponse = operations['admin/emoji/list-remote']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminEmojiAddsRequest = operations['admin___emoji___adds']['requestBody']['content']['application/json']; +type AdminEmojiListRequest = operations['admin/emoji/list']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiAddsResponse = operations['admin___emoji___adds']['responses']['200']['content']['application/json']; +type AdminEmojiListResponse = operations['admin/emoji/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminEmojiCopyRequest = operations['admin___emoji___copy']['requestBody']['content']['application/json']; +type AdminEmojiRemoveAliasesBulkRequest = operations['admin/emoji/remove-aliases-bulk']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiCopyResponse = operations['admin___emoji___copy']['responses']['200']['content']['application/json']; +type AdminEmojiSetAliasesBulkRequest = operations['admin/emoji/set-aliases-bulk']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiDeleteBulkRequest = operations['admin___emoji___delete-bulk']['requestBody']['content']['application/json']; +type AdminEmojiSetCategoryBulkRequest = operations['admin/emoji/set-category-bulk']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiDeleteRequest = operations['admin___emoji___delete']['requestBody']['content']['application/json']; +type AdminEmojiSetLicenseBulkRequest = operations['admin/emoji/set-license-bulk']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiImportZipRequest = operations['admin___emoji___import-zip']['requestBody']['content']['application/json']; +type AdminEmojiStealRequest = operations['admin/emoji/steal']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiListRemoteRequest = operations['admin___emoji___list-remote']['requestBody']['content']['application/json']; +type AdminEmojiStealResponse = operations['admin/emoji/steal']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminEmojiListRemoteResponse = operations['admin___emoji___list-remote']['responses']['200']['content']['application/json']; +type AdminEmojiUpdateRequest = operations['admin/emoji/update']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiListRequest = operations['admin___emoji___list']['requestBody']['content']['application/json']; +type AdminFederationDeleteAllFilesRequest = operations['admin/federation/delete-all-files']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiListResponse = operations['admin___emoji___list']['responses']['200']['content']['application/json']; +type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['admin/federation/refresh-remote-instance-metadata']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiRemoveAliasesBulkRequest = operations['admin___emoji___remove-aliases-bulk']['requestBody']['content']['application/json']; +type AdminFederationRemoveAllFollowingRequest = operations['admin/federation/remove-all-following']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiSetAliasesBulkRequest = operations['admin___emoji___set-aliases-bulk']['requestBody']['content']['application/json']; +type AdminFederationUpdateInstanceRequest = operations['admin/federation/update-instance']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiSetCategoryBulkRequest = operations['admin___emoji___set-category-bulk']['requestBody']['content']['application/json']; +type AdminGetIndexStatsResponse = operations['admin/get-index-stats']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminEmojiSetLicenseBulkRequest = operations['admin___emoji___set-license-bulk']['requestBody']['content']['application/json']; +type AdminGetTableStatsResponse = operations['admin/get-table-stats']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminEmojiStealRequest = operations['admin___emoji___steal']['requestBody']['content']['application/json']; +type AdminGetUserIpsRequest = operations['admin/get-user-ips']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminEmojiStealResponse = operations['admin___emoji___steal']['responses']['200']['content']['application/json']; +type AdminGetUserIpsResponse = operations['admin/get-user-ips']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminEmojiUpdateRequest = operations['admin___emoji___update']['requestBody']['content']['application/json']; +type AdminInviteCreateRequest = operations['admin/invite/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminFederationDeleteAllFilesRequest = operations['admin___federation___delete-all-files']['requestBody']['content']['application/json']; +type AdminInviteCreateResponse = operations['admin/invite/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['admin___federation___refresh-remote-instance-metadata']['requestBody']['content']['application/json']; +type AdminInviteListRequest = operations['admin/invite/list']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminFederationRemoveAllFollowingRequest = operations['admin___federation___remove-all-following']['requestBody']['content']['application/json']; +type AdminInviteListResponse = operations['admin/invite/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json']; +type AdminMetaResponse = operations['admin/meta']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json']; +type AdminPromoCreateRequest = operations['admin/promo/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminGetTableStatsResponse = operations['admin___get-table-stats']['responses']['200']['content']['application/json']; +type AdminQueueDeliverDelayedResponse = operations['admin/queue/deliver-delayed']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminGetUserIpsRequest = operations['admin___get-user-ips']['requestBody']['content']['application/json']; +type AdminQueueInboxDelayedResponse = operations['admin/queue/inbox-delayed']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminGetUserIpsResponse = operations['admin___get-user-ips']['responses']['200']['content']['application/json']; +type AdminQueuePromoteRequest = operations['admin/queue/promote']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminInviteCreateRequest = operations['admin___invite___create']['requestBody']['content']['application/json']; +type AdminQueueStatsResponse = operations['admin/queue/stats']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminInviteCreateResponse = operations['admin___invite___create']['responses']['200']['content']['application/json']; +type AdminRelaysAddRequest = operations['admin/relays/add']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminInviteListRequest = operations['admin___invite___list']['requestBody']['content']['application/json']; +type AdminRelaysAddResponse = operations['admin/relays/add']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminInviteListResponse = operations['admin___invite___list']['responses']['200']['content']['application/json']; +type AdminRelaysListResponse = operations['admin/relays/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json']; +type AdminRelaysRemoveRequest = operations['admin/relays/remove']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json']; +type AdminResetPasswordRequest = operations['admin/reset-password']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json']; +type AdminResetPasswordResponse = operations['admin/reset-password']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json']; +type AdminResolveAbuseUserReportRequest = operations['admin/resolve-abuse-user-report']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBody']['content']['application/json']; +type AdminRolesAssignRequest = operations['admin/roles/assign']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json']; +type AdminRolesCreateRequest = operations['admin/roles/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json']; +type AdminRolesCreateResponse = operations['admin/roles/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json']; +type AdminRolesDeleteRequest = operations['admin/roles/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRelaysListResponse = operations['admin___relays___list']['responses']['200']['content']['application/json']; +type AdminRolesListResponse = operations['admin/roles/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminRelaysRemoveRequest = operations['admin___relays___remove']['requestBody']['content']['application/json']; +type AdminRolesShowRequest = operations['admin/roles/show']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json']; +type AdminRolesShowResponse = operations['admin/roles/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json']; +type AdminRolesUnassignRequest = operations['admin/roles/unassign']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json']; +type AdminRolesUpdateDefaultPoliciesRequest = operations['admin/roles/update-default-policies']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRolesAssignRequest = operations['admin___roles___assign']['requestBody']['content']['application/json']; +type AdminRolesUpdateRequest = operations['admin/roles/update']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRolesCreateRequest = operations['admin___roles___create']['requestBody']['content']['application/json']; +type AdminRolesUsersRequest = operations['admin/roles/users']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRolesCreateResponse = operations['admin___roles___create']['responses']['200']['content']['application/json']; +type AdminRolesUsersResponse = operations['admin/roles/users']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminRolesDeleteRequest = operations['admin___roles___delete']['requestBody']['content']['application/json']; +type AdminSendEmailRequest = operations['admin/send-email']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRolesListResponse = operations['admin___roles___list']['responses']['200']['content']['application/json']; +type AdminServerInfoResponse = operations['admin/server-info']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminRolesShowRequest = operations['admin___roles___show']['requestBody']['content']['application/json']; +type AdminShowModerationLogsRequest = operations['admin/show-moderation-logs']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRolesShowResponse = operations['admin___roles___show']['responses']['200']['content']['application/json']; +type AdminShowModerationLogsResponse = operations['admin/show-moderation-logs']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminRolesUnassignRequest = operations['admin___roles___unassign']['requestBody']['content']['application/json']; +type AdminShowUserRequest = operations['admin/show-user']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRolesUpdateDefaultPoliciesRequest = operations['admin___roles___update-default-policies']['requestBody']['content']['application/json']; +type AdminShowUserResponse = operations['admin/show-user']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminRolesUpdateRequest = operations['admin___roles___update']['requestBody']['content']['application/json']; +type AdminShowUsersRequest = operations['admin/show-users']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminRolesUsersRequest = operations['admin___roles___users']['requestBody']['content']['application/json']; +type AdminShowUsersResponse = operations['admin/show-users']['responses']['200']['content']['application/json']; // @public (undocumented) -type AdminRolesUsersResponse = operations['admin___roles___users']['responses']['200']['content']['application/json']; +type AdminSuspendUserRequest = operations['admin/suspend-user']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json']; +type AdminUnsetUserAvatarRequest = operations['admin/unset-user-avatar']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json']; +type AdminUnsetUserBannerRequest = operations['admin/unset-user-banner']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminSetUserSensitiveRequest = operations['admin___set-user-sensitive']['requestBody']['content']['application/json']; +type AdminUnsuspendUserRequest = operations['admin/unsuspend-user']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json']; +type AdminUpdateMetaRequest = operations['admin/update-meta']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminShowUserResponse = operations['admin___show-user']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminShowUsersResponse = operations['admin___show-users']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookListRequest = operations['admin___system-webhook___list']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminUnsetUserSensitiveRequest = operations['admin___unset-user-sensitive']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AdminUpdateUserNoteRequest = operations['admin___update-user-note']['requestBody']['content']['application/json']; +type AdminUpdateUserNoteRequest = operations['admin/update-user-note']['requestBody']['content']['application/json']; // @public (undocumented) type Announcement = components['schemas']['Announcement']; @@ -429,50 +363,44 @@ type AnnouncementsRequest = operations['announcements']['requestBody']['content' // @public (undocumented) type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json']; -// @public (undocumented) -type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json']; - -// @public (undocumented) -type AnnouncementsShowResponse = operations['announcements___show']['responses']['200']['content']['application/json']; - // @public (undocumented) type Antenna = components['schemas']['Antenna']; // @public (undocumented) -type AntennasCreateRequest = operations['antennas___create']['requestBody']['content']['application/json']; +type AntennasCreateRequest = operations['antennas/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AntennasCreateResponse = operations['antennas___create']['responses']['200']['content']['application/json']; +type AntennasCreateResponse = operations['antennas/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type AntennasDeleteRequest = operations['antennas___delete']['requestBody']['content']['application/json']; +type AntennasDeleteRequest = operations['antennas/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type AntennasListResponse = operations['antennas___list']['responses']['200']['content']['application/json']; +type AntennasListResponse = operations['antennas/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type AntennasNotesRequest = operations['antennas___notes']['requestBody']['content']['application/json']; +type AntennasNotesRequest = operations['antennas/notes']['requestBody']['content']['application/json']; // @public (undocumented) -type AntennasNotesResponse = operations['antennas___notes']['responses']['200']['content']['application/json']; +type AntennasNotesResponse = operations['antennas/notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type AntennasShowRequest = operations['antennas___show']['requestBody']['content']['application/json']; +type AntennasShowRequest = operations['antennas/show']['requestBody']['content']['application/json']; // @public (undocumented) -type AntennasShowResponse = operations['antennas___show']['responses']['200']['content']['application/json']; +type AntennasShowResponse = operations['antennas/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type AntennasUpdateRequest = operations['antennas___update']['requestBody']['content']['application/json']; +type AntennasUpdateRequest = operations['antennas/update']['requestBody']['content']['application/json']; // @public (undocumented) -type AntennasUpdateResponse = operations['antennas___update']['responses']['200']['content']['application/json']; +type AntennasUpdateResponse = operations['antennas/update']['responses']['200']['content']['application/json']; // @public (undocumented) -type ApGetRequest = operations['ap___get']['requestBody']['content']['application/json']; +type ApGetRequest = operations['ap/get']['requestBody']['content']['application/json']; // @public (undocumented) -type ApGetResponse = operations['ap___get']['responses']['200']['content']['application/json']; +type ApGetResponse = operations['ap/get']['responses']['200']['content']['application/json']; declare namespace api { export { @@ -513,73 +441,64 @@ type APIError = { type App = components['schemas']['App']; // @public (undocumented) -type AppCreateRequest = operations['app___create']['requestBody']['content']['application/json']; +type AppCreateRequest = operations['app/create']['requestBody']['content']['application/json']; // @public (undocumented) -type AppCreateResponse = operations['app___create']['responses']['200']['content']['application/json']; +type AppCreateResponse = operations['app/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type AppShowRequest = operations['app___show']['requestBody']['content']['application/json']; +type AppShowRequest = operations['app/show']['requestBody']['content']['application/json']; // @public (undocumented) -type AppShowResponse = operations['app___show']['responses']['200']['content']['application/json']; +type AppShowResponse = operations['app/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type ApShowRequest = operations['ap___show']['requestBody']['content']['application/json']; +type ApShowRequest = operations['ap/show']['requestBody']['content']['application/json']; // @public (undocumented) -type ApShowResponse = operations['ap___show']['responses']['200']['content']['application/json']; +type ApShowResponse = operations['ap/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type AuthAcceptRequest = operations['auth___accept']['requestBody']['content']['application/json']; +type AuthAcceptRequest = operations['auth/accept']['requestBody']['content']['application/json']; // @public (undocumented) -type AuthSessionGenerateRequest = operations['auth___session___generate']['requestBody']['content']['application/json']; +type AuthSessionGenerateRequest = operations['auth/session/generate']['requestBody']['content']['application/json']; // @public (undocumented) -type AuthSessionGenerateResponse = operations['auth___session___generate']['responses']['200']['content']['application/json']; +type AuthSessionGenerateResponse = operations['auth/session/generate']['responses']['200']['content']['application/json']; // @public (undocumented) -type AuthSessionShowRequest = operations['auth___session___show']['requestBody']['content']['application/json']; +type AuthSessionShowRequest = operations['auth/session/show']['requestBody']['content']['application/json']; // @public (undocumented) -type AuthSessionShowResponse = operations['auth___session___show']['responses']['200']['content']['application/json']; +type AuthSessionShowResponse = operations['auth/session/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type AuthSessionUserkeyRequest = operations['auth___session___userkey']['requestBody']['content']['application/json']; +type AuthSessionUserkeyRequest = operations['auth/session/userkey']['requestBody']['content']['application/json']; // @public (undocumented) -type AuthSessionUserkeyResponse = operations['auth___session___userkey']['responses']['200']['content']['application/json']; +type AuthSessionUserkeyResponse = operations['auth/session/userkey']['responses']['200']['content']['application/json']; // @public (undocumented) type Blocking = components['schemas']['Blocking']; // @public (undocumented) -type BlockingCreateRequest = operations['blocking___create']['requestBody']['content']['application/json']; - -// @public (undocumented) -type BlockingCreateResponse = operations['blocking___create']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type BlockingDeleteRequest = operations['blocking___delete']['requestBody']['content']['application/json']; - -// @public (undocumented) -type BlockingDeleteResponse = operations['blocking___delete']['responses']['200']['content']['application/json']; +type BlockingCreateRequest = operations['blocking/create']['requestBody']['content']['application/json']; // @public (undocumented) -type BlockingListRequest = operations['blocking___list']['requestBody']['content']['application/json']; +type BlockingCreateResponse = operations['blocking/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json']; +type BlockingDeleteRequest = operations['blocking/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type BubbleGameRankingRequest = operations['bubble-game___ranking']['requestBody']['content']['application/json']; +type BlockingDeleteResponse = operations['blocking/delete']['responses']['200']['content']['application/json']; // @public (undocumented) -type BubbleGameRankingResponse = operations['bubble-game___ranking']['responses']['200']['content']['application/json']; +type BlockingListRequest = operations['blocking/list']['requestBody']['content']['application/json']; // @public (undocumented) -type BubbleGameRegisterRequest = operations['bubble-game___register']['requestBody']['content']['application/json']; +type BlockingListResponse = operations['blocking/list']['responses']['200']['content']['application/json']; // @public (undocumented) type Channel = components['schemas']['Channel']; @@ -587,7 +506,7 @@ type Channel = components['schemas']['Channel']; // Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export abstract class ChannelConnection = AnyOf> extends EventEmitter { +export abstract class ChannelConnection = any> extends EventEmitter { constructor(stream: Stream, channel: string, name?: string); // (undocumented) channel: string; @@ -616,10 +535,10 @@ export type Channels = { mention: (payload: Note) => void; reply: (payload: Note) => void; renote: (payload: Note) => void; - follow: (payload: UserDetailedNotMe) => void; - followed: (payload: UserDetailed | UserLite) => void; - unfollow: (payload: UserDetailed) => void; - meUpdated: (payload: UserDetailed) => void; + follow: (payload: User) => void; + followed: (payload: User) => void; + unfollow: (payload: User) => void; + meUpdated: (payload: MeDetailed) => void; pageEvent: (payload: PageEvent) => void; urlUploadFinished: (payload: { marker: string; @@ -629,8 +548,6 @@ export type Channels = { unreadNotification: (payload: Notification_2) => void; unreadMention: (payload: Note['id']) => void; readAllUnreadMentions: () => void; - notificationFlushed: () => void; - notificationDeleted: () => void; unreadSpecifiedNote: (payload: Note['id']) => void; readAllUnreadSpecifiedNotes: () => void; readAllMessagingMessages: () => void; @@ -658,7 +575,6 @@ export type Channels = { withRenotes?: boolean; withFiles?: boolean; withCats?: boolean; - withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -671,7 +587,6 @@ export type Channels = { withReplies?: boolean; withFiles?: boolean; withCats?: boolean; - withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -684,7 +599,6 @@ export type Channels = { withReplies?: boolean; withFiles?: boolean; withCats?: boolean; - withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -696,7 +610,6 @@ export type Channels = { withRenotes?: boolean; withFiles?: boolean; withCats?: boolean; - withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -724,7 +637,6 @@ export type Channels = { params: { listId: string; withFiles?: boolean; - withRenotes?: boolean; withCats?: boolean; }; events: { @@ -776,7 +688,7 @@ export type Channels = { fileUpdated: (payload: DriveFile) => void; folderCreated: (payload: DriveFolder) => void; folderDeleted: (payload: DriveFolder['id']) => void; - folderUpdated: (payload: DriveFolder) => void; + folderUpdated: (payload: DriveFile) => void; }; receives: null; }; @@ -821,184 +733,184 @@ export type Channels = { }; // @public (undocumented) -type ChannelsCreateRequest = operations['channels___create']['requestBody']['content']['application/json']; +type ChannelsCreateRequest = operations['channels/create']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsCreateResponse = operations['channels___create']['responses']['200']['content']['application/json']; +type ChannelsCreateResponse = operations['channels/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChannelsFavoriteRequest = operations['channels___favorite']['requestBody']['content']['application/json']; +type ChannelsFavoriteRequest = operations['channels/favorite']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsFeaturedResponse = operations['channels___featured']['responses']['200']['content']['application/json']; +type ChannelsFeaturedResponse = operations['channels/featured']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChannelsFollowedRequest = operations['channels___followed']['requestBody']['content']['application/json']; +type ChannelsFollowedRequest = operations['channels/followed']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsFollowedResponse = operations['channels___followed']['responses']['200']['content']['application/json']; +type ChannelsFollowedResponse = operations['channels/followed']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChannelsFollowRequest = operations['channels___follow']['requestBody']['content']['application/json']; +type ChannelsFollowRequest = operations['channels/follow']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsMyFavoritesResponse = operations['channels___my-favorites']['responses']['200']['content']['application/json']; +type ChannelsMyFavoritesResponse = operations['channels/my-favorites']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChannelsOwnedRequest = operations['channels___owned']['requestBody']['content']['application/json']; +type ChannelsOwnedRequest = operations['channels/owned']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsOwnedResponse = operations['channels___owned']['responses']['200']['content']['application/json']; +type ChannelsOwnedResponse = operations['channels/owned']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChannelsSearchRequest = operations['channels___search']['requestBody']['content']['application/json']; +type ChannelsSearchRequest = operations['channels/search']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsSearchResponse = operations['channels___search']['responses']['200']['content']['application/json']; +type ChannelsSearchResponse = operations['channels/search']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChannelsShowRequest = operations['channels___show']['requestBody']['content']['application/json']; +type ChannelsShowRequest = operations['channels/show']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsShowResponse = operations['channels___show']['responses']['200']['content']['application/json']; +type ChannelsShowResponse = operations['channels/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChannelsTimelineRequest = operations['channels___timeline']['requestBody']['content']['application/json']; +type ChannelsTimelineRequest = operations['channels/timeline']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsTimelineResponse = operations['channels___timeline']['responses']['200']['content']['application/json']; +type ChannelsTimelineResponse = operations['channels/timeline']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChannelsUnfavoriteRequest = operations['channels___unfavorite']['requestBody']['content']['application/json']; +type ChannelsUnfavoriteRequest = operations['channels/unfavorite']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsUnfollowRequest = operations['channels___unfollow']['requestBody']['content']['application/json']; +type ChannelsUnfollowRequest = operations['channels/unfollow']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsUpdateRequest = operations['channels___update']['requestBody']['content']['application/json']; +type ChannelsUpdateRequest = operations['channels/update']['requestBody']['content']['application/json']; // @public (undocumented) -type ChannelsUpdateResponse = operations['channels___update']['responses']['200']['content']['application/json']; +type ChannelsUpdateResponse = operations['channels/update']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsActiveUsersRequest = operations['charts___active-users']['requestBody']['content']['application/json']; +type ChartsActiveUsersRequest = operations['charts/active-users']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsActiveUsersResponse = operations['charts___active-users']['responses']['200']['content']['application/json']; +type ChartsActiveUsersResponse = operations['charts/active-users']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsApRequestRequest = operations['charts___ap-request']['requestBody']['content']['application/json']; +type ChartsApRequestRequest = operations['charts/ap-request']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsApRequestResponse = operations['charts___ap-request']['responses']['200']['content']['application/json']; +type ChartsApRequestResponse = operations['charts/ap-request']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsDriveRequest = operations['charts___drive']['requestBody']['content']['application/json']; +type ChartsDriveRequest = operations['charts/drive']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsDriveResponse = operations['charts___drive']['responses']['200']['content']['application/json']; +type ChartsDriveResponse = operations['charts/drive']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsFederationRequest = operations['charts___federation']['requestBody']['content']['application/json']; +type ChartsFederationRequest = operations['charts/federation']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsFederationResponse = operations['charts___federation']['responses']['200']['content']['application/json']; +type ChartsFederationResponse = operations['charts/federation']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsInstanceRequest = operations['charts___instance']['requestBody']['content']['application/json']; +type ChartsInstanceRequest = operations['charts/instance']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsInstanceResponse = operations['charts___instance']['responses']['200']['content']['application/json']; +type ChartsInstanceResponse = operations['charts/instance']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsNotesRequest = operations['charts___notes']['requestBody']['content']['application/json']; +type ChartsNotesRequest = operations['charts/notes']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsNotesResponse = operations['charts___notes']['responses']['200']['content']['application/json']; +type ChartsNotesResponse = operations['charts/notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsUserDriveRequest = operations['charts___user___drive']['requestBody']['content']['application/json']; +type ChartsUserDriveRequest = operations['charts/user/drive']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsUserDriveResponse = operations['charts___user___drive']['responses']['200']['content']['application/json']; +type ChartsUserDriveResponse = operations['charts/user/drive']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsUserFollowingRequest = operations['charts___user___following']['requestBody']['content']['application/json']; +type ChartsUserFollowingRequest = operations['charts/user/following']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsUserFollowingResponse = operations['charts___user___following']['responses']['200']['content']['application/json']; +type ChartsUserFollowingResponse = operations['charts/user/following']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsUserNotesRequest = operations['charts___user___notes']['requestBody']['content']['application/json']; +type ChartsUserNotesRequest = operations['charts/user/notes']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsUserNotesResponse = operations['charts___user___notes']['responses']['200']['content']['application/json']; +type ChartsUserNotesResponse = operations['charts/user/notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsUserPvRequest = operations['charts___user___pv']['requestBody']['content']['application/json']; +type ChartsUserPvRequest = operations['charts/user/pv']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsUserPvResponse = operations['charts___user___pv']['responses']['200']['content']['application/json']; +type ChartsUserPvResponse = operations['charts/user/pv']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsUserReactionsRequest = operations['charts___user___reactions']['requestBody']['content']['application/json']; +type ChartsUserReactionsRequest = operations['charts/user/reactions']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsUserReactionsResponse = operations['charts___user___reactions']['responses']['200']['content']['application/json']; +type ChartsUserReactionsResponse = operations['charts/user/reactions']['responses']['200']['content']['application/json']; // @public (undocumented) -type ChartsUsersRequest = operations['charts___users']['requestBody']['content']['application/json']; +type ChartsUsersRequest = operations['charts/users']['requestBody']['content']['application/json']; // @public (undocumented) -type ChartsUsersResponse = operations['charts___users']['responses']['200']['content']['application/json']; +type ChartsUsersResponse = operations['charts/users']['responses']['200']['content']['application/json']; // @public (undocumented) type Clip = components['schemas']['Clip']; // @public (undocumented) -type ClipsAddNoteRequest = operations['clips___add-note']['requestBody']['content']['application/json']; +type ClipsAddNoteRequest = operations['clips/add-note']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsCreateRequest = operations['clips___create']['requestBody']['content']['application/json']; +type ClipsCreateRequest = operations['clips/create']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsCreateResponse = operations['clips___create']['responses']['200']['content']['application/json']; +type ClipsCreateResponse = operations['clips/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type ClipsDeleteRequest = operations['clips___delete']['requestBody']['content']['application/json']; +type ClipsDeleteRequest = operations['clips/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsFavoriteRequest = operations['clips___favorite']['requestBody']['content']['application/json']; +type ClipsFavoriteRequest = operations['clips/favorite']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsListResponse = operations['clips___list']['responses']['200']['content']['application/json']; +type ClipsListResponse = operations['clips/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type ClipsMyFavoritesResponse = operations['clips___my-favorites']['responses']['200']['content']['application/json']; +type ClipsMyFavoritesResponse = operations['clips/my-favorites']['responses']['200']['content']['application/json']; // @public (undocumented) -type ClipsNotesRequest = operations['clips___notes']['requestBody']['content']['application/json']; +type ClipsNotesRequest = operations['clips/notes']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsNotesResponse = operations['clips___notes']['responses']['200']['content']['application/json']; +type ClipsNotesResponse = operations['clips/notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type ClipsRemoveNoteRequest = operations['clips___remove-note']['requestBody']['content']['application/json']; +type ClipsRemoveNoteRequest = operations['clips/remove-note']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsShowRequest = operations['clips___show']['requestBody']['content']['application/json']; +type ClipsShowRequest = operations['clips/show']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsShowResponse = operations['clips___show']['responses']['200']['content']['application/json']; +type ClipsShowResponse = operations['clips/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type ClipsUnfavoriteRequest = operations['clips___unfavorite']['requestBody']['content']['application/json']; +type ClipsUnfavoriteRequest = operations['clips/unfavorite']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsUpdateRequest = operations['clips___update']['requestBody']['content']['application/json']; +type ClipsUpdateRequest = operations['clips/update']['requestBody']['content']['application/json']; // @public (undocumented) -type ClipsUpdateResponse = operations['clips___update']['responses']['200']['content']['application/json']; +type ClipsUpdateResponse = operations['clips/update']['responses']['200']['content']['application/json']; // @public (undocumented) type DateString = string; @@ -1007,109 +919,109 @@ type DateString = string; type DriveFile = components['schemas']['DriveFile']; // @public (undocumented) -type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json']; +type DriveFilesAttachedNotesRequest = operations['drive/files/attached-notes']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFilesAttachedNotesResponse = operations['drive___files___attached-notes']['responses']['200']['content']['application/json']; +type DriveFilesAttachedNotesResponse = operations['drive/files/attached-notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFilesCheckExistenceRequest = operations['drive___files___check-existence']['requestBody']['content']['application/json']; +type DriveFilesCheckExistenceRequest = operations['drive/files/check-existence']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFilesCheckExistenceResponse = operations['drive___files___check-existence']['responses']['200']['content']['application/json']; +type DriveFilesCheckExistenceResponse = operations['drive/files/check-existence']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFilesCreateRequest = operations['drive___files___create']['requestBody']['content']['multipart/form-data']; +type DriveFilesCreateRequest = operations['drive/files/create']['requestBody']['content']['multipart/form-data']; // @public (undocumented) -type DriveFilesCreateResponse = operations['drive___files___create']['responses']['200']['content']['application/json']; +type DriveFilesCreateResponse = operations['drive/files/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFilesDeleteRequest = operations['drive___files___delete']['requestBody']['content']['application/json']; +type DriveFilesDeleteRequest = operations['drive/files/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFilesFindByHashRequest = operations['drive___files___find-by-hash']['requestBody']['content']['application/json']; +type DriveFilesFindByHashRequest = operations['drive/files/find-by-hash']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFilesFindByHashResponse = operations['drive___files___find-by-hash']['responses']['200']['content']['application/json']; +type DriveFilesFindByHashResponse = operations['drive/files/find-by-hash']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFilesFindRequest = operations['drive___files___find']['requestBody']['content']['application/json']; +type DriveFilesFindRequest = operations['drive/files/find']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFilesFindResponse = operations['drive___files___find']['responses']['200']['content']['application/json']; +type DriveFilesFindResponse = operations['drive/files/find']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json']; +type DriveFilesRequest = operations['drive/files']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFilesResponse = operations['drive___files']['responses']['200']['content']['application/json']; +type DriveFilesResponse = operations['drive/files']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFilesShowRequest = operations['drive___files___show']['requestBody']['content']['application/json']; +type DriveFilesShowRequest = operations['drive/files/show']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFilesShowResponse = operations['drive___files___show']['responses']['200']['content']['application/json']; +type DriveFilesShowResponse = operations['drive/files/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFilesUpdateRequest = operations['drive___files___update']['requestBody']['content']['application/json']; +type DriveFilesUpdateRequest = operations['drive/files/update']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFilesUpdateResponse = operations['drive___files___update']['responses']['200']['content']['application/json']; +type DriveFilesUpdateResponse = operations['drive/files/update']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFilesUploadFromUrlRequest = operations['drive___files___upload-from-url']['requestBody']['content']['application/json']; +type DriveFilesUploadFromUrlRequest = operations['drive/files/upload-from-url']['requestBody']['content']['application/json']; // @public (undocumented) type DriveFolder = components['schemas']['DriveFolder']; // @public (undocumented) -type DriveFoldersCreateRequest = operations['drive___folders___create']['requestBody']['content']['application/json']; +type DriveFoldersCreateRequest = operations['drive/folders/create']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFoldersCreateResponse = operations['drive___folders___create']['responses']['200']['content']['application/json']; +type DriveFoldersCreateResponse = operations['drive/folders/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFoldersDeleteRequest = operations['drive___folders___delete']['requestBody']['content']['application/json']; +type DriveFoldersDeleteRequest = operations['drive/folders/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFoldersFindRequest = operations['drive___folders___find']['requestBody']['content']['application/json']; +type DriveFoldersFindRequest = operations['drive/folders/find']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFoldersFindResponse = operations['drive___folders___find']['responses']['200']['content']['application/json']; +type DriveFoldersFindResponse = operations['drive/folders/find']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFoldersRequest = operations['drive___folders']['requestBody']['content']['application/json']; +type DriveFoldersRequest = operations['drive/folders']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFoldersResponse = operations['drive___folders']['responses']['200']['content']['application/json']; +type DriveFoldersResponse = operations['drive/folders']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFoldersShowRequest = operations['drive___folders___show']['requestBody']['content']['application/json']; +type DriveFoldersShowRequest = operations['drive/folders/show']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFoldersShowResponse = operations['drive___folders___show']['responses']['200']['content']['application/json']; +type DriveFoldersShowResponse = operations['drive/folders/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveFoldersUpdateRequest = operations['drive___folders___update']['requestBody']['content']['application/json']; +type DriveFoldersUpdateRequest = operations['drive/folders/update']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveFoldersUpdateResponse = operations['drive___folders___update']['responses']['200']['content']['application/json']; +type DriveFoldersUpdateResponse = operations['drive/folders/update']['responses']['200']['content']['application/json']; // @public (undocumented) type DriveResponse = operations['drive']['responses']['200']['content']['application/json']; // @public (undocumented) -type DriveStreamRequest = operations['drive___stream']['requestBody']['content']['application/json']; +type DriveStreamRequest = operations['drive/stream']['requestBody']['content']['application/json']; // @public (undocumented) -type DriveStreamResponse = operations['drive___stream']['responses']['200']['content']['application/json']; +type DriveStreamResponse = operations['drive/stream']['responses']['200']['content']['application/json']; // @public (undocumented) -type EmailAddressAvailableRequest = operations['email-address___available']['requestBody']['content']['application/json']; +type EmailAddressAvailableRequest = operations['email-address/available']['requestBody']['content']['application/json']; // @public (undocumented) -type EmailAddressAvailableResponse = operations['email-address___available']['responses']['200']['content']['application/json']; +type EmailAddressAvailableResponse = operations['email-address/available']['responses']['200']['content']['application/json']; // @public (undocumented) type EmojiAdded = { @@ -1174,24 +1086,6 @@ export type Endpoints = Overwrite; - res: AdminRolesCreateResponse; - }; }>; // @public (undocumented) @@ -1211,13 +1105,6 @@ declare namespace entities { EmojiUpdated, EmojiDeleted, AnnouncementCreated, - SignupRequest, - SignupResponse, - SignupPendingRequest, - SignupPendingResponse, - SigninRequest, - SigninResponse, - PartialRolePolicyOverride, EmptyRequest, EmptyResponse, AdminMetaResponse, @@ -1229,15 +1116,6 @@ declare namespace entities { AdminAbuseReportResolverUpdateRequest, AdminAbuseUserReportsRequest, AdminAbuseUserReportsResponse, - AdminAbuseReportNotificationRecipientListRequest, - AdminAbuseReportNotificationRecipientListResponse, - AdminAbuseReportNotificationRecipientShowRequest, - AdminAbuseReportNotificationRecipientShowResponse, - AdminAbuseReportNotificationRecipientCreateRequest, - AdminAbuseReportNotificationRecipientCreateResponse, - AdminAbuseReportNotificationRecipientUpdateRequest, - AdminAbuseReportNotificationRecipientUpdateResponse, - AdminAbuseReportNotificationRecipientDeleteRequest, AdminAccountsCreateRequest, AdminAccountsCreateResponse, AdminAccountsDeleteRequest, @@ -1269,9 +1147,7 @@ declare namespace entities { AdminDriveShowFileResponse, AdminEmojiAddAliasesBulkRequest, AdminEmojiAddRequest, - AdminEmojiAddResponse, AdminEmojiAddsRequest, - AdminEmojiAddsResponse, AdminEmojiCopyRequest, AdminEmojiCopyResponse, AdminEmojiDeleteBulkRequest, @@ -1322,10 +1198,9 @@ declare namespace entities { AdminShowUsersResponse, AdminSuspendUserRequest, AdminUnsuspendUserRequest, - AdminSetUserSensitiveRequest, - AdminUnsetUserSensitiveRequest, AdminUpdateMetaRequest, AdminDeleteAccountRequest, + AdminDeleteAccountResponse, AdminUpdateUserNoteRequest, AdminRolesCreateRequest, AdminRolesCreateResponse, @@ -1339,19 +1214,8 @@ declare namespace entities { AdminRolesUpdateDefaultPoliciesRequest, AdminRolesUsersRequest, AdminRolesUsersResponse, - AdminSystemWebhookCreateRequest, - AdminSystemWebhookCreateResponse, - AdminSystemWebhookDeleteRequest, - AdminSystemWebhookListRequest, - AdminSystemWebhookListResponse, - AdminSystemWebhookShowRequest, - AdminSystemWebhookShowResponse, - AdminSystemWebhookUpdateRequest, - AdminSystemWebhookUpdateResponse, AnnouncementsRequest, AnnouncementsResponse, - AnnouncementsShowRequest, - AnnouncementsShowResponse, AntennasCreateRequest, AntennasCreateResponse, AntennasDeleteRequest, @@ -1534,7 +1398,6 @@ declare namespace entities { HashtagsUsersResponse, IResponse, I2faDoneRequest, - I2faDoneResponse, I2faKeyDoneRequest, I2faKeyDoneResponse, I2faPasswordLessRequest, @@ -1585,7 +1448,6 @@ declare namespace entities { IRegistryKeysWithTypeRequest, IRegistryKeysWithTypeResponse, IRegistryKeysRequest, - IRegistryKeysResponse, IRegistryRemoveRequest, IRegistryScopesWithDomainResponse, IRegistrySetRequest, @@ -1694,7 +1556,6 @@ declare namespace entities { NotesUserListTimelineRequest, NotesUserListTimelineResponse, NotificationsCreateRequest, - NotificationsDeleteRequest, PagePushRequest, PagesCreateRequest, PagesCreateResponse, @@ -1824,9 +1685,6 @@ declare namespace entities { FetchExternalResourcesRequest, FetchExternalResourcesResponse, RetentionResponse, - BubbleGameRegisterRequest, - BubbleGameRankingRequest, - BubbleGameRankingResponse, Error_2 as Error, UserLite, UserDetailedNotMeOnly, @@ -1854,7 +1712,6 @@ declare namespace entities { Hashtag, InviteCode, Page, - PageBlock, Channel, QueueCount, Antenna, @@ -1865,22 +1722,8 @@ declare namespace entities { EmojiDetailed, Flash, Signin, - RoleCondFormulaLogics, - RoleCondFormulaValueNot, - RoleCondFormulaValueIsLocalOrRemote, - RoleCondFormulaValueUserSettingBooleanSchema, - RoleCondFormulaValueAssignedRole, - RoleCondFormulaValueCreated, - RoleCondFormulaFollowersOrFollowingOrNotes, - RoleCondFormulaValue, RoleLite, - Role, - RolePolicies, - MetaLite, - MetaDetailedOnly, - MetaDetailed, - SystemWebhook, - AbuseReportNotificationRecipient + Role } } export { entities } @@ -1889,46 +1732,46 @@ export { entities } type Error_2 = components['schemas']['Error']; // @public (undocumented) -type FederationFollowersRequest = operations['federation___followers']['requestBody']['content']['application/json']; +type FederationFollowersRequest = operations['federation/followers']['requestBody']['content']['application/json']; // @public (undocumented) -type FederationFollowersResponse = operations['federation___followers']['responses']['200']['content']['application/json']; +type FederationFollowersResponse = operations['federation/followers']['responses']['200']['content']['application/json']; // @public (undocumented) -type FederationFollowingRequest = operations['federation___following']['requestBody']['content']['application/json']; +type FederationFollowingRequest = operations['federation/following']['requestBody']['content']['application/json']; // @public (undocumented) -type FederationFollowingResponse = operations['federation___following']['responses']['200']['content']['application/json']; +type FederationFollowingResponse = operations['federation/following']['responses']['200']['content']['application/json']; // @public (undocumented) type FederationInstance = components['schemas']['FederationInstance']; // @public (undocumented) -type FederationInstancesRequest = operations['federation___instances']['requestBody']['content']['application/json']; +type FederationInstancesRequest = operations['federation/instances']['requestBody']['content']['application/json']; // @public (undocumented) -type FederationInstancesResponse = operations['federation___instances']['responses']['200']['content']['application/json']; +type FederationInstancesResponse = operations['federation/instances']['responses']['200']['content']['application/json']; // @public (undocumented) -type FederationShowInstanceRequest = operations['federation___show-instance']['requestBody']['content']['application/json']; +type FederationShowInstanceRequest = operations['federation/show-instance']['requestBody']['content']['application/json']; // @public (undocumented) -type FederationShowInstanceResponse = operations['federation___show-instance']['responses']['200']['content']['application/json']; +type FederationShowInstanceResponse = operations['federation/show-instance']['responses']['200']['content']['application/json']; // @public (undocumented) -type FederationStatsRequest = operations['federation___stats']['requestBody']['content']['application/json']; +type FederationStatsRequest = operations['federation/stats']['requestBody']['content']['application/json']; // @public (undocumented) -type FederationStatsResponse = operations['federation___stats']['responses']['200']['content']['application/json']; +type FederationStatsResponse = operations['federation/stats']['responses']['200']['content']['application/json']; // @public (undocumented) -type FederationUpdateRemoteUserRequest = operations['federation___update-remote-user']['requestBody']['content']['application/json']; +type FederationUpdateRemoteUserRequest = operations['federation/update-remote-user']['requestBody']['content']['application/json']; // @public (undocumented) -type FederationUsersRequest = operations['federation___users']['requestBody']['content']['application/json']; +type FederationUsersRequest = operations['federation/users']['requestBody']['content']['application/json']; // @public (undocumented) -type FederationUsersResponse = operations['federation___users']['responses']['200']['content']['application/json']; +type FederationUsersResponse = operations['federation/users']['responses']['200']['content']['application/json']; // @public (undocumented) type FetchExternalResourcesRequest = operations['fetch-external-resources']['requestBody']['content']['application/json']; @@ -1939,7 +1782,7 @@ type FetchExternalResourcesResponse = operations['fetch-external-resources']['re // @public (undocumented) type FetchLike = (input: string, init?: { method?: string; - body?: Blob | FormData | string; + body?: string; credentials?: RequestCredentials; cache?: RequestCache; headers: { @@ -1960,49 +1803,49 @@ type FetchRssResponse = operations['fetch-rss']['responses']['200']['content'][' type Flash = components['schemas']['Flash']; // @public (undocumented) -type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json']; +type FlashCreateRequest = operations['flash/create']['requestBody']['content']['application/json']; // @public (undocumented) -type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json']; +type FlashCreateResponse = operations['flash/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json']; +type FlashDeleteRequest = operations['flash/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json']; +type FlashFeaturedResponse = operations['flash/featured']['responses']['200']['content']['application/json']; // @public (undocumented) -type FlashGenTokenRequest = operations['flash___gen-token']['requestBody']['content']['application/json']; +type FlashGenTokenRequest = operations['flash/gen-token']['requestBody']['content']['application/json']; // @public (undocumented) -type FlashGenTokenResponse = operations['flash___gen-token']['responses']['200']['content']['application/json']; +type FlashGenTokenResponse = operations['flash/gen-token']['responses']['200']['content']['application/json']; // @public (undocumented) -type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json']; +type FlashLikeRequest = operations['flash/like']['requestBody']['content']['application/json']; // @public (undocumented) -type FlashMyLikesRequest = operations['flash___my-likes']['requestBody']['content']['application/json']; +type FlashMyLikesRequest = operations['flash/my-likes']['requestBody']['content']['application/json']; // @public (undocumented) -type FlashMyLikesResponse = operations['flash___my-likes']['responses']['200']['content']['application/json']; +type FlashMyLikesResponse = operations['flash/my-likes']['responses']['200']['content']['application/json']; // @public (undocumented) -type FlashMyRequest = operations['flash___my']['requestBody']['content']['application/json']; +type FlashMyRequest = operations['flash/my']['requestBody']['content']['application/json']; // @public (undocumented) -type FlashMyResponse = operations['flash___my']['responses']['200']['content']['application/json']; +type FlashMyResponse = operations['flash/my']['responses']['200']['content']['application/json']; // @public (undocumented) -type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json']; +type FlashShowRequest = operations['flash/show']['requestBody']['content']['application/json']; // @public (undocumented) -type FlashShowResponse = operations['flash___show']['responses']['200']['content']['application/json']; +type FlashShowResponse = operations['flash/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type FlashUnlikeRequest = operations['flash___unlike']['requestBody']['content']['application/json']; +type FlashUnlikeRequest = operations['flash/unlike']['requestBody']['content']['application/json']; // @public (undocumented) -type FlashUpdateRequest = operations['flash___update']['requestBody']['content']['application/json']; +type FlashUpdateRequest = operations['flash/update']['requestBody']['content']['application/json']; // @public (undocumented) export const followersVisibilities: readonly ["public", "followers", "private"]; @@ -2011,97 +1854,97 @@ export const followersVisibilities: readonly ["public", "followers", "private"]; type Following = components['schemas']['Following']; // @public (undocumented) -type FollowingCreateRequest = operations['following___create']['requestBody']['content']['application/json']; +type FollowingCreateRequest = operations['following/create']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingCreateResponse = operations['following___create']['responses']['200']['content']['application/json']; +type FollowingCreateResponse = operations['following/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type FollowingDeleteRequest = operations['following___delete']['requestBody']['content']['application/json']; +type FollowingDeleteRequest = operations['following/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingDeleteResponse = operations['following___delete']['responses']['200']['content']['application/json']; +type FollowingDeleteResponse = operations['following/delete']['responses']['200']['content']['application/json']; // @public (undocumented) -type FollowingInvalidateRequest = operations['following___invalidate']['requestBody']['content']['application/json']; +type FollowingInvalidateRequest = operations['following/invalidate']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingInvalidateResponse = operations['following___invalidate']['responses']['200']['content']['application/json']; +type FollowingInvalidateResponse = operations['following/invalidate']['responses']['200']['content']['application/json']; // @public (undocumented) -type FollowingRequestsAcceptRequest = operations['following___requests___accept']['requestBody']['content']['application/json']; +type FollowingRequestsAcceptRequest = operations['following/requests/accept']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingRequestsCancelRequest = operations['following___requests___cancel']['requestBody']['content']['application/json']; +type FollowingRequestsCancelRequest = operations['following/requests/cancel']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingRequestsCancelResponse = operations['following___requests___cancel']['responses']['200']['content']['application/json']; +type FollowingRequestsCancelResponse = operations['following/requests/cancel']['responses']['200']['content']['application/json']; // @public (undocumented) -type FollowingRequestsListRequest = operations['following___requests___list']['requestBody']['content']['application/json']; +type FollowingRequestsListRequest = operations['following/requests/list']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingRequestsListResponse = operations['following___requests___list']['responses']['200']['content']['application/json']; +type FollowingRequestsListResponse = operations['following/requests/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json']; +type FollowingRequestsRejectRequest = operations['following/requests/reject']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingUpdateAllRequest = operations['following___update-all']['requestBody']['content']['application/json']; +type FollowingUpdateAllRequest = operations['following/update-all']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingUpdateRequest = operations['following___update']['requestBody']['content']['application/json']; +type FollowingUpdateRequest = operations['following/update']['requestBody']['content']['application/json']; // @public (undocumented) -type FollowingUpdateResponse = operations['following___update']['responses']['200']['content']['application/json']; +type FollowingUpdateResponse = operations['following/update']['responses']['200']['content']['application/json']; // @public (undocumented) export const followingVisibilities: readonly ["public", "followers", "private"]; // @public (undocumented) -type GalleryFeaturedRequest = operations['gallery___featured']['requestBody']['content']['application/json']; +type GalleryFeaturedRequest = operations['gallery/featured']['requestBody']['content']['application/json']; // @public (undocumented) -type GalleryFeaturedResponse = operations['gallery___featured']['responses']['200']['content']['application/json']; +type GalleryFeaturedResponse = operations['gallery/featured']['responses']['200']['content']['application/json']; // @public (undocumented) -type GalleryPopularResponse = operations['gallery___popular']['responses']['200']['content']['application/json']; +type GalleryPopularResponse = operations['gallery/popular']['responses']['200']['content']['application/json']; // @public (undocumented) type GalleryPost = components['schemas']['GalleryPost']; // @public (undocumented) -type GalleryPostsCreateRequest = operations['gallery___posts___create']['requestBody']['content']['application/json']; +type GalleryPostsCreateRequest = operations['gallery/posts/create']['requestBody']['content']['application/json']; // @public (undocumented) -type GalleryPostsCreateResponse = operations['gallery___posts___create']['responses']['200']['content']['application/json']; +type GalleryPostsCreateResponse = operations['gallery/posts/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type GalleryPostsDeleteRequest = operations['gallery___posts___delete']['requestBody']['content']['application/json']; +type GalleryPostsDeleteRequest = operations['gallery/posts/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type GalleryPostsLikeRequest = operations['gallery___posts___like']['requestBody']['content']['application/json']; +type GalleryPostsLikeRequest = operations['gallery/posts/like']['requestBody']['content']['application/json']; // @public (undocumented) -type GalleryPostsRequest = operations['gallery___posts']['requestBody']['content']['application/json']; +type GalleryPostsRequest = operations['gallery/posts']['requestBody']['content']['application/json']; // @public (undocumented) -type GalleryPostsResponse = operations['gallery___posts']['responses']['200']['content']['application/json']; +type GalleryPostsResponse = operations['gallery/posts']['responses']['200']['content']['application/json']; // @public (undocumented) -type GalleryPostsShowRequest = operations['gallery___posts___show']['requestBody']['content']['application/json']; +type GalleryPostsShowRequest = operations['gallery/posts/show']['requestBody']['content']['application/json']; // @public (undocumented) -type GalleryPostsShowResponse = operations['gallery___posts___show']['responses']['200']['content']['application/json']; +type GalleryPostsShowResponse = operations['gallery/posts/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type GalleryPostsUnlikeRequest = operations['gallery___posts___unlike']['requestBody']['content']['application/json']; +type GalleryPostsUnlikeRequest = operations['gallery/posts/unlike']['requestBody']['content']['application/json']; // @public (undocumented) -type GalleryPostsUpdateRequest = operations['gallery___posts___update']['requestBody']['content']['application/json']; +type GalleryPostsUpdateRequest = operations['gallery/posts/update']['requestBody']['content']['application/json']; // @public (undocumented) -type GalleryPostsUpdateResponse = operations['gallery___posts___update']['responses']['200']['content']['application/json']; +type GalleryPostsUpdateResponse = operations['gallery/posts/update']['responses']['200']['content']['application/json']; // @public (undocumented) type GetAvatarDecorationsResponse = operations['get-avatar-decorations']['responses']['200']['content']['application/json']; @@ -2113,286 +1956,280 @@ type GetOnlineUsersCountResponse = operations['get-online-users-count']['respons type Hashtag = components['schemas']['Hashtag']; // @public (undocumented) -type HashtagsListRequest = operations['hashtags___list']['requestBody']['content']['application/json']; - -// @public (undocumented) -type HashtagsListResponse = operations['hashtags___list']['responses']['200']['content']['application/json']; +type HashtagsListRequest = operations['hashtags/list']['requestBody']['content']['application/json']; // @public (undocumented) -type HashtagsSearchRequest = operations['hashtags___search']['requestBody']['content']['application/json']; +type HashtagsListResponse = operations['hashtags/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type HashtagsSearchResponse = operations['hashtags___search']['responses']['200']['content']['application/json']; +type HashtagsSearchRequest = operations['hashtags/search']['requestBody']['content']['application/json']; // @public (undocumented) -type HashtagsShowRequest = operations['hashtags___show']['requestBody']['content']['application/json']; +type HashtagsSearchResponse = operations['hashtags/search']['responses']['200']['content']['application/json']; // @public (undocumented) -type HashtagsShowResponse = operations['hashtags___show']['responses']['200']['content']['application/json']; +type HashtagsShowRequest = operations['hashtags/show']['requestBody']['content']['application/json']; // @public (undocumented) -type HashtagsTrendResponse = operations['hashtags___trend']['responses']['200']['content']['application/json']; +type HashtagsShowResponse = operations['hashtags/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type HashtagsUsersRequest = operations['hashtags___users']['requestBody']['content']['application/json']; +type HashtagsTrendResponse = operations['hashtags/trend']['responses']['200']['content']['application/json']; // @public (undocumented) -type HashtagsUsersResponse = operations['hashtags___users']['responses']['200']['content']['application/json']; +type HashtagsUsersRequest = operations['hashtags/users']['requestBody']['content']['application/json']; // @public (undocumented) -type I2faDoneRequest = operations['i___2fa___done']['requestBody']['content']['application/json']; +type HashtagsUsersResponse = operations['hashtags/users']['responses']['200']['content']['application/json']; // @public (undocumented) -type I2faDoneResponse = operations['i___2fa___done']['responses']['200']['content']['application/json']; +type I2faDoneRequest = operations['i/2fa/done']['requestBody']['content']['application/json']; // @public (undocumented) -type I2faKeyDoneRequest = operations['i___2fa___key-done']['requestBody']['content']['application/json']; +type I2faKeyDoneRequest = operations['i/2fa/key-done']['requestBody']['content']['application/json']; // @public (undocumented) -type I2faKeyDoneResponse = operations['i___2fa___key-done']['responses']['200']['content']['application/json']; +type I2faKeyDoneResponse = operations['i/2fa/key-done']['responses']['200']['content']['application/json']; // @public (undocumented) -type I2faPasswordLessRequest = operations['i___2fa___password-less']['requestBody']['content']['application/json']; +type I2faPasswordLessRequest = operations['i/2fa/password-less']['requestBody']['content']['application/json']; // @public (undocumented) -type I2faRegisterKeyRequest = operations['i___2fa___register-key']['requestBody']['content']['application/json']; +type I2faRegisterKeyRequest = operations['i/2fa/register-key']['requestBody']['content']['application/json']; // @public (undocumented) -type I2faRegisterKeyResponse = operations['i___2fa___register-key']['responses']['200']['content']['application/json']; +type I2faRegisterKeyResponse = operations['i/2fa/register-key']['responses']['200']['content']['application/json']; // @public (undocumented) -type I2faRegisterRequest = operations['i___2fa___register']['requestBody']['content']['application/json']; +type I2faRegisterRequest = operations['i/2fa/register']['requestBody']['content']['application/json']; // @public (undocumented) -type I2faRegisterResponse = operations['i___2fa___register']['responses']['200']['content']['application/json']; +type I2faRegisterResponse = operations['i/2fa/register']['responses']['200']['content']['application/json']; // @public (undocumented) -type I2faRemoveKeyRequest = operations['i___2fa___remove-key']['requestBody']['content']['application/json']; +type I2faRemoveKeyRequest = operations['i/2fa/remove-key']['requestBody']['content']['application/json']; // @public (undocumented) -type I2faUnregisterRequest = operations['i___2fa___unregister']['requestBody']['content']['application/json']; +type I2faUnregisterRequest = operations['i/2fa/unregister']['requestBody']['content']['application/json']; // @public (undocumented) -type I2faUpdateKeyRequest = operations['i___2fa___update-key']['requestBody']['content']['application/json']; +type I2faUpdateKeyRequest = operations['i/2fa/update-key']['requestBody']['content']['application/json']; // @public (undocumented) -type IAppsRequest = operations['i___apps']['requestBody']['content']['application/json']; +type IAppsRequest = operations['i/apps']['requestBody']['content']['application/json']; // @public (undocumented) -type IAppsResponse = operations['i___apps']['responses']['200']['content']['application/json']; +type IAppsResponse = operations['i/apps']['responses']['200']['content']['application/json']; // @public (undocumented) -type IAuthorizedAppsRequest = operations['i___authorized-apps']['requestBody']['content']['application/json']; +type IAuthorizedAppsRequest = operations['i/authorized-apps']['requestBody']['content']['application/json']; // @public (undocumented) -type IAuthorizedAppsResponse = operations['i___authorized-apps']['responses']['200']['content']['application/json']; +type IAuthorizedAppsResponse = operations['i/authorized-apps']['responses']['200']['content']['application/json']; // @public (undocumented) -type IChangePasswordRequest = operations['i___change-password']['requestBody']['content']['application/json']; +type IChangePasswordRequest = operations['i/change-password']['requestBody']['content']['application/json']; // @public (undocumented) -type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json']; +type IClaimAchievementRequest = operations['i/claim-achievement']['requestBody']['content']['application/json']; // @public (undocumented) type ID = string; // @public (undocumented) -type IDeleteAccountRequest = operations['i___delete-account']['requestBody']['content']['application/json']; +type IDeleteAccountRequest = operations['i/delete-account']['requestBody']['content']['application/json']; // @public (undocumented) -type IExportFollowingRequest = operations['i___export-following']['requestBody']['content']['application/json']; +type IExportFollowingRequest = operations['i/export-following']['requestBody']['content']['application/json']; // @public (undocumented) -type IFavoritesRequest = operations['i___favorites']['requestBody']['content']['application/json']; +type IFavoritesRequest = operations['i/favorites']['requestBody']['content']['application/json']; // @public (undocumented) -type IFavoritesResponse = operations['i___favorites']['responses']['200']['content']['application/json']; +type IFavoritesResponse = operations['i/favorites']['responses']['200']['content']['application/json']; // @public (undocumented) -type IGalleryLikesRequest = operations['i___gallery___likes']['requestBody']['content']['application/json']; +type IGalleryLikesRequest = operations['i/gallery/likes']['requestBody']['content']['application/json']; // @public (undocumented) -type IGalleryLikesResponse = operations['i___gallery___likes']['responses']['200']['content']['application/json']; +type IGalleryLikesResponse = operations['i/gallery/likes']['responses']['200']['content']['application/json']; // @public (undocumented) -type IGalleryPostsRequest = operations['i___gallery___posts']['requestBody']['content']['application/json']; +type IGalleryPostsRequest = operations['i/gallery/posts']['requestBody']['content']['application/json']; // @public (undocumented) -type IGalleryPostsResponse = operations['i___gallery___posts']['responses']['200']['content']['application/json']; +type IGalleryPostsResponse = operations['i/gallery/posts']['responses']['200']['content']['application/json']; // @public (undocumented) -type IImportAntennasRequest = operations['i___import-antennas']['requestBody']['content']['application/json']; +type IImportAntennasRequest = operations['i/import-antennas']['requestBody']['content']['application/json']; // @public (undocumented) -type IImportBlockingRequest = operations['i___import-blocking']['requestBody']['content']['application/json']; +type IImportBlockingRequest = operations['i/import-blocking']['requestBody']['content']['application/json']; // @public (undocumented) -type IImportFollowingRequest = operations['i___import-following']['requestBody']['content']['application/json']; +type IImportFollowingRequest = operations['i/import-following']['requestBody']['content']['application/json']; // @public (undocumented) -type IImportMutingRequest = operations['i___import-muting']['requestBody']['content']['application/json']; +type IImportMutingRequest = operations['i/import-muting']['requestBody']['content']['application/json']; // @public (undocumented) -type IImportUserListsRequest = operations['i___import-user-lists']['requestBody']['content']['application/json']; +type IImportUserListsRequest = operations['i/import-user-lists']['requestBody']['content']['application/json']; // @public (undocumented) -type IMoveRequest = operations['i___move']['requestBody']['content']['application/json']; +type IMoveRequest = operations['i/move']['requestBody']['content']['application/json']; // @public (undocumented) -type IMoveResponse = operations['i___move']['responses']['200']['content']['application/json']; +type IMoveResponse = operations['i/move']['responses']['200']['content']['application/json']; // @public (undocumented) -type INotificationsGroupedRequest = operations['i___notifications-grouped']['requestBody']['content']['application/json']; +type INotificationsGroupedRequest = operations['i/notifications-grouped']['requestBody']['content']['application/json']; // @public (undocumented) -type INotificationsGroupedResponse = operations['i___notifications-grouped']['responses']['200']['content']['application/json']; +type INotificationsGroupedResponse = operations['i/notifications-grouped']['responses']['200']['content']['application/json']; // @public (undocumented) -type INotificationsRequest = operations['i___notifications']['requestBody']['content']['application/json']; +type INotificationsRequest = operations['i/notifications']['requestBody']['content']['application/json']; // @public (undocumented) -type INotificationsResponse = operations['i___notifications']['responses']['200']['content']['application/json']; +type INotificationsResponse = operations['i/notifications']['responses']['200']['content']['application/json']; // @public (undocumented) type InviteCode = components['schemas']['InviteCode']; // @public (undocumented) -type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json']; +type InviteCreateResponse = operations['invite/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json']; +type InviteDeleteRequest = operations['invite/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type InviteLimitResponse = operations['invite___limit']['responses']['200']['content']['application/json']; +type InviteLimitResponse = operations['invite/limit']['responses']['200']['content']['application/json']; // @public (undocumented) -type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json']; +type InviteListRequest = operations['invite/list']['requestBody']['content']['application/json']; // @public (undocumented) -type InviteListResponse = operations['invite___list']['responses']['200']['content']['application/json']; +type InviteListResponse = operations['invite/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type IPageLikesRequest = operations['i___page-likes']['requestBody']['content']['application/json']; +type IPageLikesRequest = operations['i/page-likes']['requestBody']['content']['application/json']; // @public (undocumented) -type IPageLikesResponse = operations['i___page-likes']['responses']['200']['content']['application/json']; +type IPageLikesResponse = operations['i/page-likes']['responses']['200']['content']['application/json']; // @public (undocumented) -type IPagesRequest = operations['i___pages']['requestBody']['content']['application/json']; +type IPagesRequest = operations['i/pages']['requestBody']['content']['application/json']; // @public (undocumented) -type IPagesResponse = operations['i___pages']['responses']['200']['content']['application/json']; +type IPagesResponse = operations['i/pages']['responses']['200']['content']['application/json']; // @public (undocumented) -type IPinRequest = operations['i___pin']['requestBody']['content']['application/json']; +type IPinRequest = operations['i/pin']['requestBody']['content']['application/json']; // @public (undocumented) -type IPinResponse = operations['i___pin']['responses']['200']['content']['application/json']; +type IPinResponse = operations['i/pin']['responses']['200']['content']['application/json']; // @public (undocumented) -type IReadAnnouncementRequest = operations['i___read-announcement']['requestBody']['content']['application/json']; +type IReadAnnouncementRequest = operations['i/read-announcement']['requestBody']['content']['application/json']; // @public (undocumented) -type IRegenerateTokenRequest = operations['i___regenerate-token']['requestBody']['content']['application/json']; +type IRegenerateTokenRequest = operations['i/regenerate-token']['requestBody']['content']['application/json']; // @public (undocumented) -type IRegistryGetAllRequest = operations['i___registry___get-all']['requestBody']['content']['application/json']; +type IRegistryGetAllRequest = operations['i/registry/get-all']['requestBody']['content']['application/json']; // @public (undocumented) -type IRegistryGetAllResponse = operations['i___registry___get-all']['responses']['200']['content']['application/json']; +type IRegistryGetAllResponse = operations['i/registry/get-all']['responses']['200']['content']['application/json']; // @public (undocumented) -type IRegistryGetDetailRequest = operations['i___registry___get-detail']['requestBody']['content']['application/json']; +type IRegistryGetDetailRequest = operations['i/registry/get-detail']['requestBody']['content']['application/json']; // @public (undocumented) -type IRegistryGetDetailResponse = operations['i___registry___get-detail']['responses']['200']['content']['application/json']; +type IRegistryGetDetailResponse = operations['i/registry/get-detail']['responses']['200']['content']['application/json']; // @public (undocumented) -type IRegistryGetRequest = operations['i___registry___get']['requestBody']['content']['application/json']; +type IRegistryGetRequest = operations['i/registry/get']['requestBody']['content']['application/json']; // @public (undocumented) -type IRegistryGetResponse = operations['i___registry___get']['responses']['200']['content']['application/json']; +type IRegistryGetResponse = operations['i/registry/get']['responses']['200']['content']['application/json']; // @public (undocumented) -type IRegistryKeysRequest = operations['i___registry___keys']['requestBody']['content']['application/json']; +type IRegistryKeysRequest = operations['i/registry/keys']['requestBody']['content']['application/json']; // @public (undocumented) -type IRegistryKeysResponse = operations['i___registry___keys']['responses']['200']['content']['application/json']; +type IRegistryKeysWithTypeRequest = operations['i/registry/keys-with-type']['requestBody']['content']['application/json']; // @public (undocumented) -type IRegistryKeysWithTypeRequest = operations['i___registry___keys-with-type']['requestBody']['content']['application/json']; +type IRegistryKeysWithTypeResponse = operations['i/registry/keys-with-type']['responses']['200']['content']['application/json']; // @public (undocumented) -type IRegistryKeysWithTypeResponse = operations['i___registry___keys-with-type']['responses']['200']['content']['application/json']; +type IRegistryRemoveRequest = operations['i/registry/remove']['requestBody']['content']['application/json']; // @public (undocumented) -type IRegistryRemoveRequest = operations['i___registry___remove']['requestBody']['content']['application/json']; +type IRegistryScopesWithDomainResponse = operations['i/registry/scopes-with-domain']['responses']['200']['content']['application/json']; // @public (undocumented) -type IRegistryScopesWithDomainResponse = operations['i___registry___scopes-with-domain']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type IRegistrySetRequest = operations['i___registry___set']['requestBody']['content']['application/json']; +type IRegistrySetRequest = operations['i/registry/set']['requestBody']['content']['application/json']; // @public (undocumented) type IResponse = operations['i']['responses']['200']['content']['application/json']; // @public (undocumented) -type IRevokeTokenRequest = operations['i___revoke-token']['requestBody']['content']['application/json']; +type IRevokeTokenRequest = operations['i/revoke-token']['requestBody']['content']['application/json']; // @public (undocumented) -function isAPIError(reason: Record): reason is APIError; +function isAPIError(reason: any): reason is APIError; // @public (undocumented) -type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['content']['application/json']; +type ISigninHistoryRequest = operations['i/signin-history']['requestBody']['content']['application/json']; // @public (undocumented) -type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json']; +type ISigninHistoryResponse = operations['i/signin-history']['responses']['200']['content']['application/json']; // @public (undocumented) -type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; +type IUnpinRequest = operations['i/unpin']['requestBody']['content']['application/json']; // @public (undocumented) -type IUnpinResponse = operations['i___unpin']['responses']['200']['content']['application/json']; +type IUnpinResponse = operations['i/unpin']['responses']['200']['content']['application/json']; // @public (undocumented) -type IUpdateEmailRequest = operations['i___update-email']['requestBody']['content']['application/json']; +type IUpdateEmailRequest = operations['i/update-email']['requestBody']['content']['application/json']; // @public (undocumented) -type IUpdateEmailResponse = operations['i___update-email']['responses']['200']['content']['application/json']; +type IUpdateEmailResponse = operations['i/update-email']['responses']['200']['content']['application/json']; // @public (undocumented) -type IUpdateRequest = operations['i___update']['requestBody']['content']['application/json']; +type IUpdateRequest = operations['i/update']['requestBody']['content']['application/json']; // @public (undocumented) -type IUpdateResponse = operations['i___update']['responses']['200']['content']['application/json']; +type IUpdateResponse = operations['i/update']['responses']['200']['content']['application/json']; // @public (undocumented) -type IUserGroupInvitesRequest = operations['i___user-group-invites']['requestBody']['content']['application/json']; +type IUserGroupInvitesRequest = operations['i/user-group-invites']['requestBody']['content']['application/json']; // @public (undocumented) -type IUserGroupInvitesResponse = operations['i___user-group-invites']['responses']['200']['content']['application/json']; +type IUserGroupInvitesResponse = operations['i/user-group-invites']['responses']['200']['content']['application/json']; // @public (undocumented) -type IWebhooksCreateRequest = operations['i___webhooks___create']['requestBody']['content']['application/json']; +type IWebhooksCreateRequest = operations['i/webhooks/create']['requestBody']['content']['application/json']; // @public (undocumented) -type IWebhooksCreateResponse = operations['i___webhooks___create']['responses']['200']['content']['application/json']; +type IWebhooksCreateResponse = operations['i/webhooks/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json']; +type IWebhooksDeleteRequest = operations['i/webhooks/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type IWebhooksListResponse = operations['i___webhooks___list']['responses']['200']['content']['application/json']; +type IWebhooksListResponse = operations['i/webhooks/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['content']['application/json']; +type IWebhooksShowRequest = operations['i/webhooks/show']['requestBody']['content']['application/json']; // @public (undocumented) -type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json']; +type IWebhooksShowResponse = operations['i/webhooks/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json']; +type IWebhooksUpdateRequest = operations['i/webhooks/update']['requestBody']['content']['application/json']; // @public (undocumented) type MeDetailed = components['schemas']['MeDetailed']; @@ -2401,40 +2238,31 @@ type MeDetailed = components['schemas']['MeDetailed']; type MeDetailedOnly = components['schemas']['MeDetailedOnly']; // @public (undocumented) -type MessagingHistoryRequest = operations['messaging___history']['requestBody']['content']['application/json']; +type MessagingHistoryRequest = operations['messaging/history']['requestBody']['content']['application/json']; // @public (undocumented) -type MessagingHistoryResponse = operations['messaging___history']['responses']['200']['content']['application/json']; +type MessagingHistoryResponse = operations['messaging/history']['responses']['200']['content']['application/json']; // @public (undocumented) type MessagingMessage = components['schemas']['MessagingMessage']; // @public (undocumented) -type MessagingMessagesCreateRequest = operations['messaging___messages___create']['requestBody']['content']['application/json']; - -// @public (undocumented) -type MessagingMessagesCreateResponse = operations['messaging___messages___create']['responses']['200']['content']['application/json']; - -// @public (undocumented) -type MessagingMessagesDeleteRequest = operations['messaging___messages___delete']['requestBody']['content']['application/json']; - -// @public (undocumented) -type MessagingMessagesReadRequest = operations['messaging___messages___read']['requestBody']['content']['application/json']; +type MessagingMessagesCreateRequest = operations['messaging/messages/create']['requestBody']['content']['application/json']; // @public (undocumented) -type MessagingMessagesRequest = operations['messaging___messages']['requestBody']['content']['application/json']; +type MessagingMessagesCreateResponse = operations['messaging/messages/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type MessagingMessagesResponse = operations['messaging___messages']['responses']['200']['content']['application/json']; +type MessagingMessagesDeleteRequest = operations['messaging/messages/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type MetaDetailed = components['schemas']['MetaDetailed']; +type MessagingMessagesReadRequest = operations['messaging/messages/read']['requestBody']['content']['application/json']; // @public (undocumented) -type MetaDetailedOnly = components['schemas']['MetaDetailedOnly']; +type MessagingMessagesRequest = operations['messaging/messages']['requestBody']['content']['application/json']; // @public (undocumented) -type MetaLite = components['schemas']['MetaLite']; +type MessagingMessagesResponse = operations['messaging/messages']['responses']['200']['content']['application/json']; // @public (undocumented) type MetaRequest = operations['meta']['requestBody']['content']['application/json']; @@ -2443,17 +2271,17 @@ type MetaRequest = operations['meta']['requestBody']['content']['application/jso type MetaResponse = operations['meta']['responses']['200']['content']['application/json']; // @public (undocumented) -type MiauthGenTokenRequest = operations['miauth___gen-token']['requestBody']['content']['application/json']; +type MiauthGenTokenRequest = operations['miauth/gen-token']['requestBody']['content']['application/json']; // @public (undocumented) -type MiauthGenTokenResponse = operations['miauth___gen-token']['responses']['200']['content']['application/json']; +type MiauthGenTokenResponse = operations['miauth/gen-token']['responses']['200']['content']['application/json']; // @public (undocumented) type ModerationLog = { id: ID; createdAt: DateString; userId: User['id']; - user: UserDetailedNotMe | null; + user: UserDetailed | null; } & ({ type: 'updateServerSettings'; info: ModerationLogPayloads['updateServerSettings']; @@ -2529,9 +2357,6 @@ type ModerationLog = { } | { type: 'unsuspendRemoteInstance'; info: ModerationLogPayloads['unsuspendRemoteInstance']; -} | { - type: 'updateRemoteInstanceNote'; - info: ModerationLogPayloads['updateRemoteInstanceNote']; } | { type: 'markSensitiveDriveFile'; info: ModerationLogPayloads['markSensitiveDriveFile']; @@ -2566,51 +2391,36 @@ type ModerationLog = { type: 'unsetUserAvatar'; info: ModerationLogPayloads['unsetUserAvatar']; } | { - type: 'createSystemWebhook'; - info: ModerationLogPayloads['createSystemWebhook']; -} | { - type: 'updateSystemWebhook'; - info: ModerationLogPayloads['updateSystemWebhook']; -} | { - type: 'deleteSystemWebhook'; - info: ModerationLogPayloads['deleteSystemWebhook']; -} | { - type: 'createAbuseReportNotificationRecipient'; - info: ModerationLogPayloads['createAbuseReportNotificationRecipient']; -} | { - type: 'updateAbuseReportNotificationRecipient'; - info: ModerationLogPayloads['updateAbuseReportNotificationRecipient']; -} | { - type: 'deleteAbuseReportNotificationRecipient'; - info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient']; + type: 'unsetUserBanner'; + info: ModerationLogPayloads['unsetUserBanner']; }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "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", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"]; // @public (undocumented) -type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; +type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json']; // @public (undocumented) -type MuteDeleteRequest = operations['mute___delete']['requestBody']['content']['application/json']; +type MuteDeleteRequest = operations['mute/delete']['requestBody']['content']['application/json']; // @public (undocumented) export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"]; // @public (undocumented) -type MuteListRequest = operations['mute___list']['requestBody']['content']['application/json']; +type MuteListRequest = operations['mute/list']['requestBody']['content']['application/json']; // @public (undocumented) -type MuteListResponse = operations['mute___list']['responses']['200']['content']['application/json']; +type MuteListResponse = operations['mute/list']['responses']['200']['content']['application/json']; // @public (undocumented) type Muting = components['schemas']['Muting']; // @public (undocumented) -type MyAppsRequest = operations['my___apps']['requestBody']['content']['application/json']; +type MyAppsRequest = operations['my/apps']['requestBody']['content']['application/json']; // @public (undocumented) -type MyAppsResponse = operations['my___apps']['responses']['200']['content']['application/json']; +type MyAppsResponse = operations['my/apps']['responses']['200']['content']['application/json']; // @public (undocumented) type Note = components['schemas']['Note']; @@ -2622,106 +2432,106 @@ type NoteFavorite = components['schemas']['NoteFavorite']; type NoteReaction = components['schemas']['NoteReaction']; // @public (undocumented) -type NotesChildrenRequest = operations['notes___children']['requestBody']['content']['application/json']; +type NotesChildrenRequest = operations['notes/children']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesChildrenResponse = operations['notes___children']['responses']['200']['content']['application/json']; +type NotesChildrenResponse = operations['notes/children']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesClipsRequest = operations['notes___clips']['requestBody']['content']['application/json']; +type NotesClipsRequest = operations['notes/clips']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesClipsResponse = operations['notes___clips']['responses']['200']['content']['application/json']; +type NotesClipsResponse = operations['notes/clips']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesConversationRequest = operations['notes___conversation']['requestBody']['content']['application/json']; +type NotesConversationRequest = operations['notes/conversation']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesConversationResponse = operations['notes___conversation']['responses']['200']['content']['application/json']; +type NotesConversationResponse = operations['notes/conversation']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesCreateRequest = operations['notes___create']['requestBody']['content']['application/json']; +type NotesCreateRequest = operations['notes/create']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesCreateResponse = operations['notes___create']['responses']['200']['content']['application/json']; +type NotesCreateResponse = operations['notes/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json']; +type NotesDeleteRequest = operations['notes/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesEventsSearchRequest = operations['notes___events___search']['requestBody']['content']['application/json']; +type NotesEventsSearchRequest = operations['notes/events/search']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesEventsSearchResponse = operations['notes___events___search']['responses']['200']['content']['application/json']; +type NotesEventsSearchResponse = operations['notes/events/search']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesFavoritesCreateRequest = operations['notes___favorites___create']['requestBody']['content']['application/json']; +type NotesFavoritesCreateRequest = operations['notes/favorites/create']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesFavoritesDeleteRequest = operations['notes___favorites___delete']['requestBody']['content']['application/json']; +type NotesFavoritesDeleteRequest = operations['notes/favorites/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesFeaturedRequest = operations['notes___featured']['requestBody']['content']['application/json']; +type NotesFeaturedRequest = operations['notes/featured']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesFeaturedResponse = operations['notes___featured']['responses']['200']['content']['application/json']; +type NotesFeaturedResponse = operations['notes/featured']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesGlobalTimelineRequest = operations['notes___global-timeline']['requestBody']['content']['application/json']; +type NotesGlobalTimelineRequest = operations['notes/global-timeline']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesGlobalTimelineResponse = operations['notes___global-timeline']['responses']['200']['content']['application/json']; +type NotesGlobalTimelineResponse = operations['notes/global-timeline']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesHybridTimelineRequest = operations['notes___hybrid-timeline']['requestBody']['content']['application/json']; +type NotesHybridTimelineRequest = operations['notes/hybrid-timeline']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesHybridTimelineResponse = operations['notes___hybrid-timeline']['responses']['200']['content']['application/json']; +type NotesHybridTimelineResponse = operations['notes/hybrid-timeline']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesLocalTimelineRequest = operations['notes___local-timeline']['requestBody']['content']['application/json']; +type NotesLocalTimelineRequest = operations['notes/local-timeline']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesLocalTimelineResponse = operations['notes___local-timeline']['responses']['200']['content']['application/json']; +type NotesLocalTimelineResponse = operations['notes/local-timeline']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesMentionsRequest = operations['notes___mentions']['requestBody']['content']['application/json']; +type NotesMentionsRequest = operations['notes/mentions']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesMentionsResponse = operations['notes___mentions']['responses']['200']['content']['application/json']; +type NotesMentionsResponse = operations['notes/mentions']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesPollsRecommendationRequest = operations['notes___polls___recommendation']['requestBody']['content']['application/json']; +type NotesPollsRecommendationRequest = operations['notes/polls/recommendation']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesPollsRecommendationResponse = operations['notes___polls___recommendation']['responses']['200']['content']['application/json']; +type NotesPollsRecommendationResponse = operations['notes/polls/recommendation']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesPollsVoteRequest = operations['notes___polls___vote']['requestBody']['content']['application/json']; +type NotesPollsVoteRequest = operations['notes/polls/vote']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesReactionsCreateRequest = operations['notes___reactions___create']['requestBody']['content']['application/json']; +type NotesReactionsCreateRequest = operations['notes/reactions/create']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesReactionsDeleteRequest = operations['notes___reactions___delete']['requestBody']['content']['application/json']; +type NotesReactionsDeleteRequest = operations['notes/reactions/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesReactionsRequest = operations['notes___reactions']['requestBody']['content']['application/json']; +type NotesReactionsRequest = operations['notes/reactions']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesReactionsResponse = operations['notes___reactions']['responses']['200']['content']['application/json']; +type NotesReactionsResponse = operations['notes/reactions']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesRenotesRequest = operations['notes___renotes']['requestBody']['content']['application/json']; +type NotesRenotesRequest = operations['notes/renotes']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesRenotesResponse = operations['notes___renotes']['responses']['200']['content']['application/json']; +type NotesRenotesResponse = operations['notes/renotes']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesRepliesRequest = operations['notes___replies']['requestBody']['content']['application/json']; +type NotesRepliesRequest = operations['notes/replies']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesRepliesResponse = operations['notes___replies']['responses']['200']['content']['application/json']; +type NotesRepliesResponse = operations['notes/replies']['responses']['200']['content']['application/json']; // @public (undocumented) type NotesRequest = operations['notes']['requestBody']['content']['application/json']; @@ -2730,70 +2540,67 @@ type NotesRequest = operations['notes']['requestBody']['content']['application/j type NotesResponse = operations['notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json']; +type NotesSearchByTagRequest = operations['notes/search-by-tag']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesSearchByTagResponse = operations['notes___search-by-tag']['responses']['200']['content']['application/json']; +type NotesSearchByTagResponse = operations['notes/search-by-tag']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesSearchRequest = operations['notes___search']['requestBody']['content']['application/json']; +type NotesSearchRequest = operations['notes/search']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesSearchResponse = operations['notes___search']['responses']['200']['content']['application/json']; +type NotesSearchResponse = operations['notes/search']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesShowRequest = operations['notes___show']['requestBody']['content']['application/json']; +type NotesShowRequest = operations['notes/show']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesShowResponse = operations['notes___show']['responses']['200']['content']['application/json']; +type NotesShowResponse = operations['notes/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesStateRequest = operations['notes___state']['requestBody']['content']['application/json']; +type NotesStateRequest = operations['notes/state']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesStateResponse = operations['notes___state']['responses']['200']['content']['application/json']; +type NotesStateResponse = operations['notes/state']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesThreadMutingCreateRequest = operations['notes___thread-muting___create']['requestBody']['content']['application/json']; +type NotesThreadMutingCreateRequest = operations['notes/thread-muting/create']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesThreadMutingDeleteRequest = operations['notes___thread-muting___delete']['requestBody']['content']['application/json']; +type NotesThreadMutingDeleteRequest = operations['notes/thread-muting/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesTimelineRequest = operations['notes___timeline']['requestBody']['content']['application/json']; +type NotesTimelineRequest = operations['notes/timeline']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesTimelineResponse = operations['notes___timeline']['responses']['200']['content']['application/json']; +type NotesTimelineResponse = operations['notes/timeline']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesTranslateRequest = operations['notes___translate']['requestBody']['content']['application/json']; +type NotesTranslateRequest = operations['notes/translate']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesTranslateResponse = operations['notes___translate']['responses']['200']['content']['application/json']; +type NotesTranslateResponse = operations['notes/translate']['responses']['200']['content']['application/json']; // @public (undocumented) -type NotesUnrenoteRequest = operations['notes___unrenote']['requestBody']['content']['application/json']; +type NotesUnrenoteRequest = operations['notes/unrenote']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesUpdateRequest = operations['notes___update']['requestBody']['content']['application/json']; +type NotesUpdateRequest = operations['notes/update']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesUserListTimelineRequest = operations['notes___user-list-timeline']['requestBody']['content']['application/json']; +type NotesUserListTimelineRequest = operations['notes/user-list-timeline']['requestBody']['content']['application/json']; // @public (undocumented) -type NotesUserListTimelineResponse = operations['notes___user-list-timeline']['responses']['200']['content']['application/json']; +type NotesUserListTimelineResponse = operations['notes/user-list-timeline']['responses']['200']['content']['application/json']; // @public (undocumented) -export const noteVisibilities: readonly ["public", "home", "followers", "specified", "private"]; +export const noteVisibilities: readonly ["public", "home", "followers", "specified"]; // @public (undocumented) type Notification_2 = components['schemas']['Notification']; // @public (undocumented) -type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json']; - -// @public (undocumented) -type NotificationsDeleteRequest = operations['notifications___delete']['requestBody']['content']['application/json']; +type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json']; // @public (undocumented) export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"]; @@ -2801,9 +2608,6 @@ export const notificationTypes: readonly ["note", "follow", "mention", "reply", // @public (undocumented) type Page = components['schemas']['Page']; -// @public (undocumented) -type PageBlock = components['schemas']['PageBlock']; - // @public (undocumented) type PageEvent = { pageId: Page['id']; @@ -2817,46 +2621,37 @@ type PageEvent = { type PagePushRequest = operations['page-push']['requestBody']['content']['application/json']; // @public (undocumented) -type PagesCreateRequest = operations['pages___create']['requestBody']['content']['application/json']; - -// @public (undocumented) -type PagesCreateResponse = operations['pages___create']['responses']['200']['content']['application/json']; +type PagesCreateRequest = operations['pages/create']['requestBody']['content']['application/json']; // @public (undocumented) -type PagesDeleteRequest = operations['pages___delete']['requestBody']['content']['application/json']; +type PagesCreateResponse = operations['pages/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type PagesFeaturedResponse = operations['pages___featured']['responses']['200']['content']['application/json']; +type PagesDeleteRequest = operations['pages/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type PagesLikeRequest = operations['pages___like']['requestBody']['content']['application/json']; +type PagesFeaturedResponse = operations['pages/featured']['responses']['200']['content']['application/json']; // @public (undocumented) -type PagesShowRequest = operations['pages___show']['requestBody']['content']['application/json']; +type PagesLikeRequest = operations['pages/like']['requestBody']['content']['application/json']; // @public (undocumented) -type PagesShowResponse = operations['pages___show']['responses']['200']['content']['application/json']; +type PagesShowRequest = operations['pages/show']['requestBody']['content']['application/json']; // @public (undocumented) -type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content']['application/json']; +type PagesShowResponse = operations['pages/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json']; +type PagesUnlikeRequest = operations['pages/unlike']['requestBody']['content']['application/json']; // @public (undocumented) -function parse(_acct: string): Acct; +type PagesUpdateRequest = operations['pages/update']['requestBody']['content']['application/json']; -// Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -type PartialRolePolicyOverride = Partial<{ - [k in keyof RolePolicies]: Omit, 'value'> & { - value: RolePolicies[k]; - }; -}>; +function parse(acct: string): Acct; // @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: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: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: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", "read:admin:show-users", "write:admin:suspend-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"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; @@ -2865,7 +2660,7 @@ type PingResponse = operations['ping']['responses']['200']['content']['applicati type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json']; // @public (undocumented) -type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json']; +type PromoReadRequest = operations['promo/read']['requestBody']['content']['application/json']; // @public (undocumented) type QueueCount = components['schemas']['QueueCount']; @@ -2887,19 +2682,19 @@ type QueueStats = { }; // @public (undocumented) -type QueueStatsLog = QueueStats[]; +type QueueStatsLog = string[]; // @public (undocumented) -type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json']; +type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json']; // @public (undocumented) -type RenoteMuteDeleteRequest = operations['renote-mute___delete']['requestBody']['content']['application/json']; +type RenoteMuteDeleteRequest = operations['renote-mute/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type RenoteMuteListRequest = operations['renote-mute___list']['requestBody']['content']['application/json']; +type RenoteMuteListRequest = operations['renote-mute/list']['requestBody']['content']['application/json']; // @public (undocumented) -type RenoteMuteListResponse = operations['renote-mute___list']['responses']['200']['content']['application/json']; +type RenoteMuteListResponse = operations['renote-mute/list']['responses']['200']['content']['application/json']; // @public (undocumented) type RenoteMuting = components['schemas']['RenoteMuting']; @@ -2916,56 +2711,29 @@ type RetentionResponse = operations['retention']['responses']['200']['content'][ // @public (undocumented) type Role = components['schemas']['Role']; -// @public (undocumented) -type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; - -// @public (undocumented) -type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics']; - -// @public (undocumented) -type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue']; - -// @public (undocumented) -type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole']; - -// @public (undocumented) -type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated']; - -// @public (undocumented) -type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote']; - -// @public (undocumented) -type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot']; - -// @public (undocumented) -type RoleCondFormulaValueUserSettingBooleanSchema = components['schemas']['RoleCondFormulaValueUserSettingBooleanSchema']; - // @public (undocumented) type RoleLite = components['schemas']['RoleLite']; // @public (undocumented) -type RolePolicies = components['schemas']['RolePolicies']; - -// @public (undocumented) -type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json']; +type RolesListResponse = operations['roles/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type RolesNotesRequest = operations['roles___notes']['requestBody']['content']['application/json']; +type RolesNotesRequest = operations['roles/notes']['requestBody']['content']['application/json']; // @public (undocumented) -type RolesNotesResponse = operations['roles___notes']['responses']['200']['content']['application/json']; +type RolesNotesResponse = operations['roles/notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type RolesShowRequest = operations['roles___show']['requestBody']['content']['application/json']; +type RolesShowRequest = operations['roles/show']['requestBody']['content']['application/json']; // @public (undocumented) -type RolesShowResponse = operations['roles___show']['responses']['200']['content']['application/json']; +type RolesShowResponse = operations['roles/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type RolesUsersRequest = operations['roles___users']['requestBody']['content']['application/json']; +type RolesUsersRequest = operations['roles/users']['requestBody']['content']['application/json']; // @public (undocumented) -type RolesUsersResponse = operations['roles___users']['responses']['200']['content']['application/json']; +type RolesUsersResponse = operations['roles/users']['responses']['200']['content']['application/json']; // @public (undocumented) type ServerInfoResponse = operations['server-info']['responses']['200']['content']['application/json']; @@ -2988,52 +2756,11 @@ type ServerStats = { }; // @public (undocumented) -type ServerStatsLog = ServerStats[]; +type ServerStatsLog = string[]; // @public (undocumented) type Signin = components['schemas']['Signin']; -// @public (undocumented) -type SigninRequest = { - username: string; - password: string; - token?: string; -}; - -// @public (undocumented) -type SigninResponse = { - id: User['id']; - i: string; -}; - -// @public (undocumented) -type SignupPendingRequest = { - code: string; -}; - -// @public (undocumented) -type SignupPendingResponse = { - id: User['id']; - i: string; -}; - -// @public (undocumented) -type SignupRequest = { - username: string; - password: string; - host?: string; - invitationCode?: string; - emailAddress?: string; - 'hcaptcha-response'?: string | null; - 'g-recaptcha-response'?: string | null; - 'turnstile-response'?: string | null; -}; - -// @public (undocumented) -type SignupResponse = MeDetailed & { - token: string; -}; - // @public (undocumented) type StatsResponse = operations['stats']['responses']['200']['content']['application/json']; @@ -3044,7 +2771,7 @@ export class Stream extends EventEmitter { constructor(origin: string, user: { token: string; } | null, options?: { - WebSocket?: WebSocket; + WebSocket?: any; }); // (undocumented) close(): void; @@ -3067,9 +2794,9 @@ export class Stream extends EventEmitter { // (undocumented) send(typeOrPayload: string): void; // (undocumented) - send(typeOrPayload: string, payload: unknown): void; + send(typeOrPayload: string, payload: any): void; // (undocumented) - send(typeOrPayload: Record | unknown[]): void; + send(typeOrPayload: Record | any[]): void; // (undocumented) state: 'initializing' | 'reconnecting' | 'connected'; // (undocumented) @@ -3084,28 +2811,25 @@ export class Stream extends EventEmitter { type SwitchCaseResponseType = Endpoints[E]['res'] extends SwitchCase ? IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : Endpoints[E]['res']['$switch']['$default'] : Endpoints[E]['res']; // @public (undocumented) -type SwRegisterRequest = operations['sw___register']['requestBody']['content']['application/json']; - -// @public (undocumented) -type SwRegisterResponse = operations['sw___register']['responses']['200']['content']['application/json']; +type SwRegisterRequest = operations['sw/register']['requestBody']['content']['application/json']; // @public (undocumented) -type SwShowRegistrationRequest = operations['sw___show-registration']['requestBody']['content']['application/json']; +type SwRegisterResponse = operations['sw/register']['responses']['200']['content']['application/json']; // @public (undocumented) -type SwShowRegistrationResponse = operations['sw___show-registration']['responses']['200']['content']['application/json']; +type SwShowRegistrationRequest = operations['sw/show-registration']['requestBody']['content']['application/json']; // @public (undocumented) -type SwUnregisterRequest = operations['sw___unregister']['requestBody']['content']['application/json']; +type SwShowRegistrationResponse = operations['sw/show-registration']['responses']['200']['content']['application/json']; // @public (undocumented) -type SwUpdateRegistrationRequest = operations['sw___update-registration']['requestBody']['content']['application/json']; +type SwUnregisterRequest = operations['sw/unregister']['requestBody']['content']['application/json']; // @public (undocumented) -type SwUpdateRegistrationResponse = operations['sw___update-registration']['responses']['200']['content']['application/json']; +type SwUpdateRegistrationRequest = operations['sw/update-registration']['requestBody']['content']['application/json']; // @public (undocumented) -type SystemWebhook = components['schemas']['SystemWebhook']; +type SwUpdateRegistrationResponse = operations['sw/update-registration']['responses']['200']['content']['application/json']; // @public (undocumented) type TestRequest = operations['test']['requestBody']['content']['application/json']; @@ -3138,193 +2862,193 @@ type UserList = components['schemas']['UserList']; type UserLite = components['schemas']['UserLite']; // @public (undocumented) -type UsernameAvailableRequest = operations['username___available']['requestBody']['content']['application/json']; +type UsernameAvailableRequest = operations['username/available']['requestBody']['content']['application/json']; // @public (undocumented) -type UsernameAvailableResponse = operations['username___available']['responses']['200']['content']['application/json']; +type UsernameAvailableResponse = operations['username/available']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersAchievementsRequest = operations['users___achievements']['requestBody']['content']['application/json']; +type UsersAchievementsRequest = operations['users/achievements']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersAchievementsResponse = operations['users___achievements']['responses']['200']['content']['application/json']; +type UsersAchievementsResponse = operations['users/achievements']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersClipsRequest = operations['users___clips']['requestBody']['content']['application/json']; +type UsersClipsRequest = operations['users/clips']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersClipsResponse = operations['users___clips']['responses']['200']['content']['application/json']; +type UsersClipsResponse = operations['users/clips']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json']; +type UsersFeaturedNotesRequest = operations['users/featured-notes']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersFeaturedNotesResponse = operations['users___featured-notes']['responses']['200']['content']['application/json']; +type UsersFeaturedNotesResponse = operations['users/featured-notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersFlashsRequest = operations['users___flashs']['requestBody']['content']['application/json']; +type UsersFlashsRequest = operations['users/flashs']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersFlashsResponse = operations['users___flashs']['responses']['200']['content']['application/json']; +type UsersFlashsResponse = operations['users/flashs']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersFollowersRequest = operations['users___followers']['requestBody']['content']['application/json']; +type UsersFollowersRequest = operations['users/followers']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersFollowersResponse = operations['users___followers']['responses']['200']['content']['application/json']; +type UsersFollowersResponse = operations['users/followers']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersFollowingRequest = operations['users___following']['requestBody']['content']['application/json']; +type UsersFollowingRequest = operations['users/following']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersFollowingResponse = operations['users___following']['responses']['200']['content']['application/json']; +type UsersFollowingResponse = operations['users/following']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBody']['content']['application/json']; +type UsersGalleryPostsRequest = operations['users/gallery/posts']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json']; +type UsersGalleryPostsResponse = operations['users/gallery/posts']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json']; +type UsersGetFrequentlyRepliedUsersRequest = operations['users/get-frequently-replied-users']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json']; +type UsersGetFrequentlyRepliedUsersResponse = operations['users/get-frequently-replied-users']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersGroupsCreateRequest = operations['users___groups___create']['requestBody']['content']['application/json']; +type UsersGroupsCreateRequest = operations['users/groups/create']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsCreateResponse = operations['users___groups___create']['responses']['200']['content']['application/json']; +type UsersGroupsCreateResponse = operations['users/groups/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersGroupsDeleteRequest = operations['users___groups___delete']['requestBody']['content']['application/json']; +type UsersGroupsDeleteRequest = operations['users/groups/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsInvitationsAcceptRequest = operations['users___groups___invitations___accept']['requestBody']['content']['application/json']; +type UsersGroupsInvitationsAcceptRequest = operations['users/groups/invitations/accept']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsInvitationsRejectRequest = operations['users___groups___invitations___reject']['requestBody']['content']['application/json']; +type UsersGroupsInvitationsRejectRequest = operations['users/groups/invitations/reject']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsInviteRequest = operations['users___groups___invite']['requestBody']['content']['application/json']; +type UsersGroupsInviteRequest = operations['users/groups/invite']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsJoinedResponse = operations['users___groups___joined']['responses']['200']['content']['application/json']; +type UsersGroupsJoinedResponse = operations['users/groups/joined']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersGroupsLeaveRequest = operations['users___groups___leave']['requestBody']['content']['application/json']; +type UsersGroupsLeaveRequest = operations['users/groups/leave']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsOwnedResponse = operations['users___groups___owned']['responses']['200']['content']['application/json']; +type UsersGroupsOwnedResponse = operations['users/groups/owned']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersGroupsPullRequest = operations['users___groups___pull']['requestBody']['content']['application/json']; +type UsersGroupsPullRequest = operations['users/groups/pull']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsShowRequest = operations['users___groups___show']['requestBody']['content']['application/json']; +type UsersGroupsShowRequest = operations['users/groups/show']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsShowResponse = operations['users___groups___show']['responses']['200']['content']['application/json']; +type UsersGroupsShowResponse = operations['users/groups/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersGroupsTransferRequest = operations['users___groups___transfer']['requestBody']['content']['application/json']; +type UsersGroupsTransferRequest = operations['users/groups/transfer']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsTransferResponse = operations['users___groups___transfer']['responses']['200']['content']['application/json']; +type UsersGroupsTransferResponse = operations['users/groups/transfer']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersGroupsUpdateRequest = operations['users___groups___update']['requestBody']['content']['application/json']; +type UsersGroupsUpdateRequest = operations['users/groups/update']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersGroupsUpdateResponse = operations['users___groups___update']['responses']['200']['content']['application/json']; +type UsersGroupsUpdateResponse = operations['users/groups/update']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersListsCreateFromPublicRequest = operations['users___lists___create-from-public']['requestBody']['content']['application/json']; +type UsersListsCreateFromPublicRequest = operations['users/lists/create-from-public']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsCreateFromPublicResponse = operations['users___lists___create-from-public']['responses']['200']['content']['application/json']; +type UsersListsCreateFromPublicResponse = operations['users/lists/create-from-public']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersListsCreateRequest = operations['users___lists___create']['requestBody']['content']['application/json']; +type UsersListsCreateRequest = operations['users/lists/create']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsCreateResponse = operations['users___lists___create']['responses']['200']['content']['application/json']; +type UsersListsCreateResponse = operations['users/lists/create']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersListsDeleteRequest = operations['users___lists___delete']['requestBody']['content']['application/json']; +type UsersListsDeleteRequest = operations['users/lists/delete']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsFavoriteRequest = operations['users___lists___favorite']['requestBody']['content']['application/json']; +type UsersListsFavoriteRequest = operations['users/lists/favorite']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsGetMembershipsRequest = operations['users___lists___get-memberships']['requestBody']['content']['application/json']; +type UsersListsGetMembershipsRequest = operations['users/lists/get-memberships']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsGetMembershipsResponse = operations['users___lists___get-memberships']['responses']['200']['content']['application/json']; +type UsersListsGetMembershipsResponse = operations['users/lists/get-memberships']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersListsListRequest = operations['users___lists___list']['requestBody']['content']['application/json']; +type UsersListsListRequest = operations['users/lists/list']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsListResponse = operations['users___lists___list']['responses']['200']['content']['application/json']; +type UsersListsListResponse = operations['users/lists/list']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersListsPullRequest = operations['users___lists___pull']['requestBody']['content']['application/json']; +type UsersListsPullRequest = operations['users/lists/pull']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsPushRequest = operations['users___lists___push']['requestBody']['content']['application/json']; +type UsersListsPushRequest = operations['users/lists/push']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsShowRequest = operations['users___lists___show']['requestBody']['content']['application/json']; +type UsersListsShowRequest = operations['users/lists/show']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsShowResponse = operations['users___lists___show']['responses']['200']['content']['application/json']; +type UsersListsShowResponse = operations['users/lists/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersListsUnfavoriteRequest = operations['users___lists___unfavorite']['requestBody']['content']['application/json']; +type UsersListsUnfavoriteRequest = operations['users/lists/unfavorite']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsUpdateMembershipRequest = operations['users___lists___update-membership']['requestBody']['content']['application/json']; +type UsersListsUpdateMembershipRequest = operations['users/lists/update-membership']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsUpdateRequest = operations['users___lists___update']['requestBody']['content']['application/json']; +type UsersListsUpdateRequest = operations['users/lists/update']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersListsUpdateResponse = operations['users___lists___update']['responses']['200']['content']['application/json']; +type UsersListsUpdateResponse = operations['users/lists/update']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersNotesRequest = operations['users___notes']['requestBody']['content']['application/json']; +type UsersNotesRequest = operations['users/notes']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersNotesResponse = operations['users___notes']['responses']['200']['content']['application/json']; +type UsersNotesResponse = operations['users/notes']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersPagesRequest = operations['users___pages']['requestBody']['content']['application/json']; +type UsersPagesRequest = operations['users/pages']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersPagesResponse = operations['users___pages']['responses']['200']['content']['application/json']; +type UsersPagesResponse = operations['users/pages']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersReactionsRequest = operations['users___reactions']['requestBody']['content']['application/json']; +type UsersReactionsRequest = operations['users/reactions']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersReactionsResponse = operations['users___reactions']['responses']['200']['content']['application/json']; +type UsersReactionsResponse = operations['users/reactions']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersRecommendationRequest = operations['users___recommendation']['requestBody']['content']['application/json']; +type UsersRecommendationRequest = operations['users/recommendation']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersRecommendationResponse = operations['users___recommendation']['responses']['200']['content']['application/json']; +type UsersRecommendationResponse = operations['users/recommendation']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersRelationRequest = operations['users___relation']['requestBody']['content']['application/json']; +type UsersRelationRequest = operations['users/relation']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersRelationResponse = operations['users___relation']['responses']['200']['content']['application/json']; +type UsersRelationResponse = operations['users/relation']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersReportAbuseRequest = operations['users___report-abuse']['requestBody']['content']['application/json']; +type UsersReportAbuseRequest = operations['users/report-abuse']['requestBody']['content']['application/json']; // @public (undocumented) type UsersRequest = operations['users']['requestBody']['content']['application/json']; @@ -3333,41 +3057,41 @@ type UsersRequest = operations['users']['requestBody']['content']['application/j type UsersResponse = operations['users']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersSearchByUsernameAndHostRequest = operations['users___search-by-username-and-host']['requestBody']['content']['application/json']; +type UsersSearchByUsernameAndHostRequest = operations['users/search-by-username-and-host']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersSearchByUsernameAndHostResponse = operations['users___search-by-username-and-host']['responses']['200']['content']['application/json']; +type UsersSearchByUsernameAndHostResponse = operations['users/search-by-username-and-host']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersSearchRequest = operations['users___search']['requestBody']['content']['application/json']; +type UsersSearchRequest = operations['users/search']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersSearchResponse = operations['users___search']['responses']['200']['content']['application/json']; +type UsersSearchResponse = operations['users/search']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersShowRequest = operations['users___show']['requestBody']['content']['application/json']; +type UsersShowRequest = operations['users/show']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersShowResponse = operations['users___show']['responses']['200']['content']['application/json']; +type UsersShowResponse = operations['users/show']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersStatsRequest = operations['users___stats']['requestBody']['content']['application/json']; +type UsersStatsRequest = operations['users/stats']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersStatsResponse = operations['users___stats']['responses']['200']['content']['application/json']; +type UsersStatsResponse = operations['users/stats']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersTranslateRequest = operations['users___translate']['requestBody']['content']['application/json']; +type UsersTranslateRequest = operations['users/translate']['requestBody']['content']['application/json']; // @public (undocumented) -type UsersTranslateResponse = operations['users___translate']['responses']['200']['content']['application/json']; +type UsersTranslateResponse = operations['users/translate']['responses']['200']['content']['application/json']; // @public (undocumented) -type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json']; +type UsersUpdateMemoRequest = operations['users/update-memo']['requestBody']['content']['application/json']; // Warnings were encountered during analysis: // -// src/entities.ts:35:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:25:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/cherrypick-js/generator/.eslintrc.cjs b/packages/cherrypick-js/generator/.eslintrc.cjs new file mode 100644 index 0000000000..6a8b31da9c --- /dev/null +++ b/packages/cherrypick-js/generator/.eslintrc.cjs @@ -0,0 +1,9 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + extends: [ + '../../shared/.eslintrc.js', + ], +}; diff --git a/packages/cherrypick-js/generator/eslint.config.js b/packages/cherrypick-js/generator/eslint.config.js deleted file mode 100644 index 4bf78c3b91..0000000000 --- a/packages/cherrypick-js/generator/eslint.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../../shared/eslint.config.js'; - -export default [ - ...sharedConfig, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - }, -]; diff --git a/packages/cherrypick-js/generator/package.json b/packages/cherrypick-js/generator/package.json index 2aea57aac4..5a3cc2ffe2 100644 --- a/packages/cherrypick-js/generator/package.json +++ b/packages/cherrypick-js/generator/package.json @@ -4,18 +4,19 @@ "description": "CherryPick TypeGenerator", "type": "module", "scripts": { - "generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix" + "generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix" }, "devDependencies": { - "@readme/openapi-parser": "2.5.0", + "@apidevtools/swagger-parser": "10.1.0", "@types/node": "20.9.1", "@typescript-eslint/eslint-plugin": "6.11.0", "@typescript-eslint/parser": "6.11.0", - "openapi-types": "12.1.3", - "openapi-typescript": "6.7.3", - "ts-case-convert": "2.0.2", + "eslint": "8.53.0", + "typescript": "5.3.3", "tsx": "4.4.0", - "typescript": "5.3.3" + "ts-case-convert": "2.0.2", + "openapi-types": "12.1.3", + "openapi-typescript": "6.7.1" }, "files": [ "built" diff --git a/packages/cherrypick-js/generator/src/generator.ts b/packages/cherrypick-js/generator/src/generator.ts index 6573efb838..34c26f574b 100644 --- a/packages/cherrypick-js/generator/src/generator.ts +++ b/packages/cherrypick-js/generator/src/generator.ts @@ -1,11 +1,28 @@ import { mkdir, writeFile } from 'fs/promises'; -import { OpenAPIV3_1 } from 'openapi-types'; +import { OpenAPIV3 } from 'openapi-types'; import { toPascal } from 'ts-case-convert'; -import OpenAPIParser from '@readme/openapi-parser'; +import SwaggerParser from '@apidevtools/swagger-parser'; import openapiTS from 'openapi-typescript'; +function generateVersionHeaderComment(openApiDocs: OpenAPIV3.Document): string { + const contents = { + version: openApiDocs.info.version, + basedMisskeyVersion: openApiDocs.info.description, + generatedAt: new Date().toISOString(), + }; + + const lines: string[] = []; + lines.push('/*'); + for (const [key, value] of Object.entries(contents)) { + lines.push(` * ${key}: ${value}`); + } + lines.push(' */'); + + return lines.join('\n'); +} + async function generateBaseTypes( - openApiDocs: OpenAPIV3_1.Document, + openApiDocs: OpenAPIV3.Document, openApiJsonPath: string, typeFileName: string, ) { @@ -20,14 +37,10 @@ async function generateBaseTypes( } lines.push(''); - const generatedTypes = await openapiTS(openApiJsonPath, { - exportType: true, - transform(schemaObject) { - if ('format' in schemaObject && schemaObject.format === 'binary') { - return schemaObject.nullable ? 'Blob | null' : 'Blob'; - } - }, - }); + lines.push(generateVersionHeaderComment(openApiDocs)); + lines.push(''); + + const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true }); lines.push(generatedTypes); lines.push(''); @@ -35,7 +48,7 @@ async function generateBaseTypes( } async function generateSchemaEntities( - openApiDocs: OpenAPIV3_1.Document, + openApiDocs: OpenAPIV3.Document, typeFileName: string, outputPath: string, ) { @@ -47,6 +60,8 @@ async function generateSchemaEntities( const schemaNames = Object.keys(schemas); const typeAliasLines: string[] = []; + typeAliasLines.push(generateVersionHeaderComment(openApiDocs)); + typeAliasLines.push(''); typeAliasLines.push(`import { components } from '${toImportPath(typeFileName)}';`); typeAliasLines.push( ...schemaNames.map(it => `export type ${it} = components['schemas']['${it}'];`), @@ -57,29 +72,23 @@ async function generateSchemaEntities( } async function generateEndpoints( - openApiDocs: OpenAPIV3_1.Document, + openApiDocs: OpenAPIV3.Document, typeFileName: string, entitiesOutputPath: string, endpointOutputPath: string, ) { const endpoints: Endpoint[] = []; - const endpointReqMediaTypes: EndpointReqMediaType[] = []; - const endpointReqMediaTypesSet = new Set(); // cherrypick-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり - const paths = openApiDocs.paths ?? {}; + const paths = openApiDocs.paths; const postPathItems = Object.keys(paths) - .map(it => ({ - _path_: it.replace(/^\//, ''), - ...paths[it]?.post, - })) + .map(it => paths[it]?.post) .filter(filterUndefined); for (const operation of postPathItems) { - const path = operation._path_; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const operationId = operation.operationId!; - const endpoint = new Endpoint(path); + const endpoint = new Endpoint(operationId); endpoints.push(endpoint); if (isRequestBodyObject(operation.requestBody)) { @@ -87,34 +96,21 @@ async function generateEndpoints( const supportMediaTypes = Object.keys(reqContent); if (supportMediaTypes.length > 0) { // いまのところ複数のメディアタイプをとるエンドポイントは無いので決め打ちする - const req = new OperationTypeAlias( + endpoint.request = new OperationTypeAlias( operationId, - path, supportMediaTypes[0], OperationsAliasType.REQUEST, ); - endpoint.request = req; - - const reqType = new EndpointReqMediaType(path, req); - endpointReqMediaTypesSet.add(reqType.getMediaType()); - endpointReqMediaTypes.push(reqType); - } else { - endpointReqMediaTypesSet.add('application/json'); - endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); } - } else { - endpointReqMediaTypesSet.add('application/json'); - endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); } - if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) { + if (isResponseObject(operation.responses['200']) && operation.responses['200'].content) { const resContent = operation.responses['200'].content; const supportMediaTypes = Object.keys(resContent); if (supportMediaTypes.length > 0) { // いまのところ複数のメディアタイプを返すエンドポイントは無いので決め打ちする endpoint.response = new OperationTypeAlias( operationId, - path, supportMediaTypes[0], OperationsAliasType.RESPONSE, ); @@ -124,7 +120,8 @@ async function generateEndpoints( const entitiesOutputLine: string[] = []; - entitiesOutputLine.push('/* eslint @typescript-eslint/naming-convention: 0 */'); + entitiesOutputLine.push(generateVersionHeaderComment(openApiDocs)); + entitiesOutputLine.push(''); entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`); entitiesOutputLine.push(''); @@ -143,6 +140,9 @@ async function generateEndpoints( const endpointOutputLine: string[] = []; + endpointOutputLine.push(generateVersionHeaderComment(openApiDocs)); + endpointOutputLine.push(''); + endpointOutputLine.push('import type {'); endpointOutputLine.push( ...[emptyRequest, emptyResponse, ...entities].map(it => '\t' + it.generateName() + ','), @@ -157,41 +157,21 @@ async function generateEndpoints( endpointOutputLine.push('}'); endpointOutputLine.push(''); - function generateEndpointReqMediaTypesType() { - return `Record `'${t}'`).join(' | ')}>`; - } - - endpointOutputLine.push(`export const endpointReqTypes: ${generateEndpointReqMediaTypesType()} = {`); - - endpointOutputLine.push( - ...endpointReqMediaTypes.map(it => '\t' + it.toLine()), - ); - - endpointOutputLine.push('};'); - endpointOutputLine.push(''); - await writeFile(endpointOutputPath, endpointOutputLine.join('\n')); } async function generateApiClientJSDoc( - openApiDocs: OpenAPIV3_1.Document, + openApiDocs: OpenAPIV3.Document, apiClientFileName: string, endpointsFileName: string, warningsOutputPath: string, ) { - const endpoints: { - operationId: string; - path: string; - description: string; - }[] = []; + const endpoints: { operationId: string; description: string; }[] = []; // cherrypick-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり - const paths = openApiDocs.paths ?? {}; + const paths = openApiDocs.paths; const postPathItems = Object.keys(paths) - .map(it => ({ - _path_: it.replace(/^\//, ''), - ...paths[it]?.post, - })) + .map(it => paths[it]?.post) .filter(filterUndefined); for (const operation of postPathItems) { @@ -201,7 +181,6 @@ async function generateApiClientJSDoc( if (operation.description) { endpoints.push({ operationId: operationId, - path: operation._path_, description: operation.description, }); } @@ -209,6 +188,9 @@ async function generateApiClientJSDoc( const endpointOutputLine: string[] = []; + endpointOutputLine.push(generateVersionHeaderComment(openApiDocs)); + endpointOutputLine.push(''); + endpointOutputLine.push(`import type { SwitchCaseResponseType } from '${toImportPath(apiClientFileName)}';`); endpointOutputLine.push(`import type { Endpoints } from '${toImportPath(endpointsFileName)}';`); endpointOutputLine.push(''); @@ -222,7 +204,7 @@ async function generateApiClientJSDoc( ' /**', ` * ${endpoint.description.split('\n').join('\n * ')}`, ' */', - ` request(`, + ` request(`, ' endpoint: E,', ' params: P,', ' credential?: string | null,', @@ -240,21 +222,21 @@ async function generateApiClientJSDoc( await writeFile(warningsOutputPath, endpointOutputLine.join('\n')); } -function isRequestBodyObject(value: unknown): value is OpenAPIV3_1.RequestBodyObject { +function isRequestBodyObject(value: unknown): value is OpenAPIV3.RequestBodyObject { if (!value) { return false; } - const { content } = value as Record; + const { content } = value as Record; return content !== undefined; } -function isResponseObject(value: unknown): value is OpenAPIV3_1.ResponseObject { +function isResponseObject(value: unknown): value is OpenAPIV3.ResponseObject { if (!value) { return false; } - const { description } = value as Record; + const { description } = value as Record; return description !== undefined; } @@ -281,24 +263,21 @@ interface IOperationTypeAlias { class OperationTypeAlias implements IOperationTypeAlias { public readonly operationId: string; - public readonly path: string; public readonly mediaType: string; public readonly type: OperationsAliasType; constructor( operationId: string, - path: string, mediaType: string, type: OperationsAliasType, ) { this.operationId = operationId; - this.path = path; this.mediaType = mediaType; this.type = type; } generateName(): string { - const nameBase = this.path.replace(/\//g, '-'); + const nameBase = this.operationId.replace(/\//g, '-'); return toPascal(nameBase + this.type); } @@ -331,39 +310,19 @@ const emptyRequest = new EmptyTypeAlias(OperationsAliasType.REQUEST); const emptyResponse = new EmptyTypeAlias(OperationsAliasType.RESPONSE); class Endpoint { - public readonly path: string; + public readonly operationId: string; public request?: IOperationTypeAlias; public response?: IOperationTypeAlias; - constructor(path: string) { - this.path = path; + constructor(operationId: string) { + this.operationId = operationId; } toLine(): string { const reqName = this.request?.generateName() ?? emptyRequest.generateName(); const resName = this.response?.generateName() ?? emptyResponse.generateName(); - return `'${this.path}': { req: ${reqName}; res: ${resName} };`; - } -} - -class EndpointReqMediaType { - public readonly path: string; - public readonly mediaType: string; - - constructor(path: string, request: OperationTypeAlias, mediaType?: undefined); - constructor(path: string, request: undefined, mediaType: string); - constructor(path: string, request: OperationTypeAlias | undefined, mediaType?: string) { - this.path = path; - this.mediaType = mediaType ?? request?.mediaType ?? 'application/json'; - } - - getMediaType(): string { - return this.mediaType; - } - - toLine(): string { - return `'${this.path}': '${this.mediaType}',`; + return `'${this.operationId}': { req: ${reqName}; res: ${resName} };`; } } @@ -372,7 +331,7 @@ async function main() { await mkdir(generatePath, { recursive: true }); const openApiJsonPath = './api.json'; - const openApiDocs = await OpenAPIParser.parse(openApiJsonPath) as OpenAPIV3_1.Document; + const openApiDocs = await SwaggerParser.validate(openApiJsonPath) as OpenAPIV3.Document; const typeFileName = './built/autogen/types.ts'; await generateBaseTypes(openApiDocs, openApiJsonPath, typeFileName); diff --git a/packages/cherrypick-js/jest.config.cjs b/packages/cherrypick-js/jest.config.cjs index 1230a4b5e2..e5a74170ea 100644 --- a/packages/cherrypick-js/jest.config.cjs +++ b/packages/cherrypick-js/jest.config.cjs @@ -81,17 +81,7 @@ module.exports = { // ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - moduleNameMapper: { - // Do not resolve .wasm.js to .wasm by the rule below - '^(.+)\\.wasm\\.js$': '$1.wasm.js', - // SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule - // converts it again to `../../src/foo/bar` which then can be resolved to - // `.ts` files. - // See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225 - // TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can - // directly import `.ts` files without this hack. - '^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1', - }, + // moduleNameMapper: {}, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], diff --git a/packages/cherrypick-js/package.json b/packages/cherrypick-js/package.json index bad49778ef..5f14113338 100644 --- a/packages/cherrypick-js/package.json +++ b/packages/cherrypick-js/package.json @@ -1,66 +1,49 @@ { - "type": "module", "name": "cherrypick-js", - "version": "4.10.0-rc.3-engawa0.5.0", - "basedMisskeyVersion": "2024.7.0", + "version": "0.0.16-cherrypick.1", "description": "CherryPick SDK for JavaScript", - "license": "MIT", "main": "./built/index.js", "types": "./built/index.d.ts", - "exports": { - ".": { - "import": "./built/index.js", - "types": "./built/index.d.ts" - }, - "./*": { - "import": "./built/*", - "types": "./built/*" - } - }, "scripts": { - "build": "node ./build.js", - "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", + "build": "tsc", + "watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"", "tsd": "tsd", "api": "pnpm api-extractor run --local --verbose", "api-prod": "pnpm api-extractor run --verbose", - "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", - "format": "pnpm biome format", - "format:write": "pnpm biome format --write", + "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", "typecheck": "tsc --noEmit", - "lint": "pnpm typecheck && pnpm biome lint", + "lint": "pnpm typecheck && pnpm eslint", "jest": "jest --coverage --detectOpenHandles", "test": "pnpm jest && pnpm tsd", "update-autogen-code": "pnpm --filter cherrypick-js-type-generator generate && ncp generator/built/autogen src/autogen" }, "repository": { "type": "git", - "url": "https://github.com/kokonect-link/cherrypick.git", - "directory": "packages/cherrypick-js" + "url": "git+https://github.com/misskey-dev/misskey.js.git" }, "devDependencies": { - "@biomejs/biome": "1.8.3", - "@microsoft/api-extractor": "7.47.4", - "@swc/jest": "0.2.36", - "@types/jest": "29.5.12", - "@types/node": "20.14.12", - "@typescript-eslint/eslint-plugin": "7.17.0", - "@typescript-eslint/parser": "7.17.0", + "@microsoft/api-extractor": "7.38.5", + "@swc/jest": "0.2.29", + "@types/jest": "29.5.11", + "@types/node": "20.10.5", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", + "eslint": "8.56.0", "jest": "29.7.0", "jest-fetch-mock": "3.0.3", "jest-websocket-mock": "2.5.0", "mock-socket": "9.3.1", "ncp": "2.0.0", - "nodemon": "3.1.4", - "execa": "9.3.0", - "tsd": "0.31.1", - "typescript": "5.5.4", - "esbuild": "0.23.0", - "glob": "11.0.0" + "nodemon": "3.0.2", + "tsd": "0.30.0", + "typescript": "5.3.3" }, "files": [ "built" ], "dependencies": { + "@swc/cli": "0.1.63", + "@swc/core": "1.3.100", "eventemitter3": "5.0.1", "reconnecting-websocket": "4.4.0" } diff --git a/packages/cherrypick-js/src/acct.ts b/packages/cherrypick-js/src/acct.ts index aa8658cdbd..b25bc564ea 100644 --- a/packages/cherrypick-js/src/acct.ts +++ b/packages/cherrypick-js/src/acct.ts @@ -3,8 +3,7 @@ export type Acct = { host: string | null; }; -export function parse(_acct: string): Acct { - let acct = _acct; +export function parse(acct: string): Acct { if (acct.startsWith('@')) acct = acct.substring(1); const split = acct.split('@', 2); return { username: split[0], host: split[1] || null }; diff --git a/packages/cherrypick-js/src/api.ts b/packages/cherrypick-js/src/api.ts index ea1df57f3d..0d10faaada 100644 --- a/packages/cherrypick-js/src/api.ts +++ b/packages/cherrypick-js/src/api.ts @@ -1,11 +1,11 @@ -import './autogen/apiClientJSDoc.js'; +import './autogen/apiClientJSDoc'; -import { endpointReqTypes } from './autogen/endpoint.js'; -import type { SwitchCaseResponseType, Endpoints } from './api.types.js'; +import { SwitchCaseResponseType } from './api.types'; +import type { Endpoints } from './api.types'; -export type { +export { SwitchCaseResponseType, -} from './api.types.js'; +} from './api.types'; const MK_API_ERROR = Symbol(); @@ -14,23 +14,21 @@ export type APIError = { code: string; message: string; kind: 'client' | 'server'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any info: Record; }; -export function isAPIError(reason: Record): reason is APIError { +export function isAPIError(reason: any): reason is APIError { return reason[MK_API_ERROR] === true; } export type FetchLike = (input: string, init?: { method?: string; - body?: Blob | FormData | string; + body?: string; credentials?: RequestCredentials; cache?: RequestCache; headers: { [key in string]: string } }) => Promise<{ status: number; - // eslint-disable-next-line @typescript-eslint/no-explicit-any json(): Promise; }>; @@ -51,56 +49,20 @@ export class APIClient { this.fetch = opts.fetch ?? ((...args) => fetch(...args)); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private assertIsRecord(obj: T): obj is T & Record { - return obj !== null && typeof obj === 'object' && !Array.isArray(obj); - } - public request( endpoint: E, params: P = {} as P, credential?: string | null, ): Promise> { return new Promise((resolve, reject) => { - let mediaType = 'application/json'; - if (endpoint in endpointReqTypes) { - mediaType = endpointReqTypes[endpoint]; - } - let payload: FormData | string = '{}'; - - if (mediaType === 'application/json') { - payload = JSON.stringify({ - ...params, - i: credential !== undefined ? credential : this.credential, - }); - } else if (mediaType === 'multipart/form-data') { - payload = new FormData(); - const i = credential !== undefined ? credential : this.credential; - if (i != null) { - payload.append('i', i); - } - if (this.assertIsRecord(params)) { - for (const key in params) { - const value = params[key]; - - if (value == null) continue; - - if (value instanceof File || value instanceof Blob) { - payload.append(key, value); - } else if (typeof value === 'object') { - payload.append(key, JSON.stringify(value)); - } else { - payload.append(key, value); - } - } - } - } - this.fetch(`${this.origin}/api/${endpoint}`, { method: 'POST', - body: payload, + body: JSON.stringify({ + ...params, + i: credential !== undefined ? credential : this.credential, + }), headers: { - 'Content-Type': endpointReqTypes[endpoint], + 'Content-Type': 'application/json', }, credentials: 'omit', cache: 'no-cache', diff --git a/packages/cherrypick-js/src/api.types.ts b/packages/cherrypick-js/src/api.types.ts index 5ee4194db2..d97646b7cc 100644 --- a/packages/cherrypick-js/src/api.types.ts +++ b/packages/cherrypick-js/src/api.types.ts @@ -1,25 +1,16 @@ -import { Endpoints as Gen } from './autogen/endpoint.js'; -import { UserDetailed } from './autogen/models.js'; -import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js'; -import { - PartialRolePolicyOverride, - SigninRequest, - SigninResponse, - SignupPendingRequest, - SignupPendingResponse, - SignupRequest, - SignupResponse, -} from './entities.js'; +import { Endpoints as Gen } from './autogen/endpoint'; +import { UserDetailed } from './autogen/models'; +import { UsersShowRequest } from './autogen/entities'; type Overwrite = Omit< T, keyof U > & U; -type SwitchCase = { +type SwitchCase = { $switch: { - $cases: [Condition, Result][], - $default: Result; + $cases: [any, any][], + $default: any; }; }; @@ -28,13 +19,11 @@ type StrictExtract = Cond extends Union ? Union : never; type IsCaseMatched = Endpoints[E]['res'] extends SwitchCase - // eslint-disable-next-line @typescript-eslint/no-explicit-any ? IsNeverType> extends false ? true : false : false type GetCaseResult = Endpoints[E]['res'] extends SwitchCase - // eslint-disable-next-line @typescript-eslint/no-explicit-any ? StrictExtract[1] : never @@ -66,25 +55,6 @@ export type Endpoints = Overwrite< $default: UserDetailed; }; }; - }, - // api.jsonには載せないものなのでここで定義 - 'signup': { - req: SignupRequest; - res: SignupResponse; - }, - // api.jsonには載せないものなのでここで定義 - 'signup-pending': { - req: SignupPendingRequest; - res: SignupPendingResponse; - }, - // api.jsonには載せないものなのでここで定義 - 'signin': { - req: SigninRequest; - res: SigninResponse; - }, - 'admin/roles/create': { - req: Overwrite; - res: AdminRolesCreateResponse; } } > diff --git a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts index 21d71d6cab..1d3b3b5823 100644 --- a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts +++ b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts @@ -1,3 +1,9 @@ +/* + * version: 4.6.0 + * basedMisskeyVersion: 2023.12.2 + * generatedAt: 2024-01-08T10:34:58.484Z + */ + import type { SwitchCaseResponseType } from '../api.js'; import type { Endpoints } from './endpoint.js'; @@ -17,8 +23,7 @@ declare module '../api.js' { /** * No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-create* + * **Credential required**: *Yes* */ request( endpoint: E, @@ -29,8 +34,7 @@ declare module '../api.js' { /** * No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-list* + * **Credential required**: *Yes* */ request( endpoint: E, @@ -41,8 +45,7 @@ declare module '../api.js' { /** * No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *No* / **Permission**: *arr-delete* + * **Credential required**: *No* */ request( endpoint: E, @@ -53,8 +56,7 @@ declare module '../api.js' { /** * No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-update* + * **Credential required**: *Yes* */ request( endpoint: E, @@ -73,66 +75,6 @@ declare module '../api.js' { credential?: string | null, ): Promise>; - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - /** * No description provided. * @@ -819,7 +761,7 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:show-user* + * **Credential required**: *Yes* / **Permission**: *read:admin:show-users* */ request( endpoint: E, @@ -849,28 +791,6 @@ declare module '../api.js' { credential?: string | null, ): Promise>; - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - /** * No description provided. * @@ -1003,66 +923,6 @@ declare module '../api.js' { credential?: string | null, ): Promise>; - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - /** * No description provided. * @@ -1074,17 +934,6 @@ declare module '../api.js' { credential?: string | null, ): Promise>; - /** - * No description provided. - * - * **Credential required**: *No* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - /** * No description provided. * @@ -2478,18 +2327,6 @@ declare module '../api.js' { credential?: string | null, ): Promise>; - /** - * No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - /** * No description provided. * @@ -3528,28 +3365,6 @@ declare module '../api.js' { credential?: string | null, ): Promise>; - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:notifications* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:notifications* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - /** * No description provided. * @@ -4501,27 +4316,5 @@ declare module '../api.js' { params: P, credential?: string | null, ): Promise>; - - /** - * No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:account* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; - - /** - * No description provided. - * - * **Credential required**: *No* - */ - request( - endpoint: E, - params: P, - credential?: string | null, - ): Promise>; } } diff --git a/packages/cherrypick-js/src/autogen/endpoint.ts b/packages/cherrypick-js/src/autogen/endpoint.ts index 8d477e0404..3223964123 100644 --- a/packages/cherrypick-js/src/autogen/endpoint.ts +++ b/packages/cherrypick-js/src/autogen/endpoint.ts @@ -1,3 +1,9 @@ +/* + * version: 4.6.0 + * basedMisskeyVersion: 2023.12.2 + * generatedAt: 2024-01-08T10:34:58.483Z + */ + import type { EmptyRequest, EmptyResponse, @@ -10,15 +16,6 @@ import type { AdminAbuseReportResolverUpdateRequest, AdminAbuseUserReportsRequest, AdminAbuseUserReportsResponse, - AdminAbuseReportNotificationRecipientListRequest, - AdminAbuseReportNotificationRecipientListResponse, - AdminAbuseReportNotificationRecipientShowRequest, - AdminAbuseReportNotificationRecipientShowResponse, - AdminAbuseReportNotificationRecipientCreateRequest, - AdminAbuseReportNotificationRecipientCreateResponse, - AdminAbuseReportNotificationRecipientUpdateRequest, - AdminAbuseReportNotificationRecipientUpdateResponse, - AdminAbuseReportNotificationRecipientDeleteRequest, AdminAccountsCreateRequest, AdminAccountsCreateResponse, AdminAccountsDeleteRequest, @@ -50,9 +47,7 @@ import type { AdminDriveShowFileResponse, AdminEmojiAddAliasesBulkRequest, AdminEmojiAddRequest, - AdminEmojiAddResponse, AdminEmojiAddsRequest, - AdminEmojiAddsResponse, AdminEmojiCopyRequest, AdminEmojiCopyResponse, AdminEmojiDeleteBulkRequest, @@ -103,10 +98,9 @@ import type { AdminShowUsersResponse, AdminSuspendUserRequest, AdminUnsuspendUserRequest, - AdminSetUserSensitiveRequest, - AdminUnsetUserSensitiveRequest, AdminUpdateMetaRequest, AdminDeleteAccountRequest, + AdminDeleteAccountResponse, AdminUpdateUserNoteRequest, AdminRolesCreateRequest, AdminRolesCreateResponse, @@ -120,19 +114,8 @@ import type { AdminRolesUpdateDefaultPoliciesRequest, AdminRolesUsersRequest, AdminRolesUsersResponse, - AdminSystemWebhookCreateRequest, - AdminSystemWebhookCreateResponse, - AdminSystemWebhookDeleteRequest, - AdminSystemWebhookListRequest, - AdminSystemWebhookListResponse, - AdminSystemWebhookShowRequest, - AdminSystemWebhookShowResponse, - AdminSystemWebhookUpdateRequest, - AdminSystemWebhookUpdateResponse, AnnouncementsRequest, AnnouncementsResponse, - AnnouncementsShowRequest, - AnnouncementsShowResponse, AntennasCreateRequest, AntennasCreateResponse, AntennasDeleteRequest, @@ -315,7 +298,6 @@ import type { HashtagsUsersResponse, IResponse, I2faDoneRequest, - I2faDoneResponse, I2faKeyDoneRequest, I2faKeyDoneResponse, I2faPasswordLessRequest, @@ -366,7 +348,6 @@ import type { IRegistryKeysWithTypeRequest, IRegistryKeysWithTypeResponse, IRegistryKeysRequest, - IRegistryKeysResponse, IRegistryRemoveRequest, IRegistryScopesWithDomainResponse, IRegistrySetRequest, @@ -475,7 +456,6 @@ import type { NotesUserListTimelineRequest, NotesUserListTimelineResponse, NotificationsCreateRequest, - NotificationsDeleteRequest, PagePushRequest, PagesCreateRequest, PagesCreateResponse, @@ -605,9 +585,6 @@ import type { FetchExternalResourcesRequest, FetchExternalResourcesResponse, RetentionResponse, - BubbleGameRegisterRequest, - BubbleGameRankingRequest, - BubbleGameRankingResponse, } from './entities.js'; export type Endpoints = { @@ -617,11 +594,6 @@ export type Endpoints = { 'admin/abuse-report-resolver/delete': { req: AdminAbuseReportResolverDeleteRequest; res: EmptyResponse }; 'admin/abuse-report-resolver/update': { req: AdminAbuseReportResolverUpdateRequest; res: EmptyResponse }; 'admin/abuse-user-reports': { req: AdminAbuseUserReportsRequest; res: AdminAbuseUserReportsResponse }; - '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/accounts/create': { req: AdminAccountsCreateRequest; res: AdminAccountsCreateResponse }; 'admin/accounts/delete': { req: AdminAccountsDeleteRequest; res: EmptyResponse }; 'admin/accounts/find-by-email': { req: AdminAccountsFindByEmailRequest; res: AdminAccountsFindByEmailResponse }; @@ -645,8 +617,8 @@ export type Endpoints = { '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/adds': { req: AdminEmojiAddsRequest; res: AdminEmojiAddsResponse }; + 'admin/emoji/add': { req: AdminEmojiAddRequest; res: EmptyResponse }; + 'admin/emoji/adds': { req: AdminEmojiAddsRequest; res: EmptyResponse }; 'admin/emoji/copy': { req: AdminEmojiCopyRequest; res: AdminEmojiCopyResponse }; 'admin/emoji/delete-bulk': { req: AdminEmojiDeleteBulkRequest; res: EmptyResponse }; 'admin/emoji/delete': { req: AdminEmojiDeleteRequest; res: EmptyResponse }; @@ -687,10 +659,8 @@ export type Endpoints = { 'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse }; 'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse }; 'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse }; - 'admin/set-user-sensitive': { req: AdminSetUserSensitiveRequest; res: EmptyResponse }; - 'admin/unset-user-sensitive': { req: AdminUnsetUserSensitiveRequest; res: EmptyResponse }; 'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse }; - 'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse }; + 'admin/delete-account': { req: AdminDeleteAccountRequest; res: AdminDeleteAccountResponse }; 'admin/update-user-note': { req: AdminUpdateUserNoteRequest; res: EmptyResponse }; 'admin/roles/create': { req: AdminRolesCreateRequest; res: AdminRolesCreateResponse }; 'admin/roles/delete': { req: AdminRolesDeleteRequest; res: EmptyResponse }; @@ -701,13 +671,7 @@ export type Endpoints = { 'admin/roles/unassign': { req: AdminRolesUnassignRequest; res: EmptyResponse }; 'admin/roles/update-default-policies': { req: AdminRolesUpdateDefaultPoliciesRequest; res: EmptyResponse }; 'admin/roles/users': { req: AdminRolesUsersRequest; res: AdminRolesUsersResponse }; - '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 }; 'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse }; - 'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse }; 'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse }; 'antennas/delete': { req: AntennasDeleteRequest; res: EmptyResponse }; 'antennas/list': { req: EmptyRequest; res: AntennasListResponse }; @@ -816,7 +780,7 @@ export type Endpoints = { 'hashtags/trend': { req: EmptyRequest; res: HashtagsTrendResponse }; 'hashtags/users': { req: HashtagsUsersRequest; res: HashtagsUsersResponse }; 'i': { req: EmptyRequest; res: IResponse }; - 'i/2fa/done': { req: I2faDoneRequest; res: I2faDoneResponse }; + 'i/2fa/done': { req: I2faDoneRequest; res: EmptyResponse }; 'i/2fa/key-done': { req: I2faKeyDoneRequest; res: I2faKeyDoneResponse }; 'i/2fa/password-less': { req: I2faPasswordLessRequest; res: EmptyResponse }; 'i/2fa/register-key': { req: I2faRegisterKeyRequest; res: I2faRegisterKeyResponse }; @@ -833,7 +797,6 @@ export type Endpoints = { '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 }; @@ -858,7 +821,7 @@ export type Endpoints = { '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/keys': { req: IRegistryKeysRequest; res: IRegistryKeysResponse }; + 'i/registry/keys': { req: IRegistryKeysRequest; res: EmptyResponse }; 'i/registry/remove': { req: IRegistryRemoveRequest; res: EmptyResponse }; 'i/registry/scopes-with-domain': { req: EmptyRequest; res: IRegistryScopesWithDomainResponse }; 'i/registry/set': { req: IRegistrySetRequest; res: EmptyResponse }; @@ -927,8 +890,6 @@ export type Endpoints = { 'notes/unrenote': { req: NotesUnrenoteRequest; res: EmptyResponse }; 'notes/user-list-timeline': { req: NotesUserListTimelineRequest; res: NotesUserListTimelineResponse }; 'notifications/create': { req: NotificationsCreateRequest; res: EmptyResponse }; - 'notifications/delete': { req: NotificationsDeleteRequest; res: EmptyResponse }; - 'notifications/flush': { req: EmptyRequest; res: EmptyResponse }; 'notifications/mark-all-as-read': { req: EmptyRequest; res: EmptyResponse }; 'notifications/test-notification': { req: EmptyRequest; res: EmptyResponse }; 'page-push': { req: PagePushRequest; res: EmptyResponse }; @@ -1015,415 +976,4 @@ export type Endpoints = { 'fetch-rss': { req: FetchRssRequest; res: FetchRssResponse }; 'fetch-external-resources': { req: FetchExternalResourcesRequest; res: FetchExternalResourcesResponse }; 'retention': { req: EmptyRequest; res: RetentionResponse }; - 'bubble-game/register': { req: BubbleGameRegisterRequest; res: EmptyResponse }; - 'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse }; } - -export const endpointReqTypes: Record = { - 'admin/meta': 'application/json', - 'admin/abuse-report-resolver/create': 'application/json', - 'admin/abuse-report-resolver/list': 'application/json', - 'admin/abuse-report-resolver/delete': 'application/json', - 'admin/abuse-report-resolver/update': 'application/json', - 'admin/abuse-user-reports': 'application/json', - 'admin/abuse-report/notification-recipient/list': 'application/json', - 'admin/abuse-report/notification-recipient/show': 'application/json', - 'admin/abuse-report/notification-recipient/create': 'application/json', - 'admin/abuse-report/notification-recipient/update': 'application/json', - 'admin/abuse-report/notification-recipient/delete': 'application/json', - 'admin/accounts/create': 'application/json', - 'admin/accounts/delete': 'application/json', - 'admin/accounts/find-by-email': 'application/json', - 'admin/ad/create': 'application/json', - 'admin/ad/delete': 'application/json', - 'admin/ad/list': 'application/json', - 'admin/ad/update': 'application/json', - 'admin/announcements/create': 'application/json', - 'admin/announcements/delete': 'application/json', - 'admin/announcements/list': 'application/json', - 'admin/announcements/update': 'application/json', - 'admin/avatar-decorations/create': 'application/json', - 'admin/avatar-decorations/delete': 'application/json', - 'admin/avatar-decorations/list': 'application/json', - 'admin/avatar-decorations/update': 'application/json', - 'admin/delete-all-files-of-a-user': 'application/json', - 'admin/unset-user-avatar': 'application/json', - 'admin/unset-user-banner': 'application/json', - 'admin/drive/clean-remote-files': 'application/json', - 'admin/drive/cleanup': 'application/json', - 'admin/drive/files': 'application/json', - 'admin/drive/show-file': 'application/json', - 'admin/emoji/add-aliases-bulk': 'application/json', - 'admin/emoji/add': 'application/json', - 'admin/emoji/adds': 'application/json', - 'admin/emoji/copy': 'application/json', - 'admin/emoji/delete-bulk': 'application/json', - 'admin/emoji/delete': 'application/json', - 'admin/emoji/import-zip': 'application/json', - 'admin/emoji/list-remote': 'application/json', - 'admin/emoji/list': 'application/json', - 'admin/emoji/remove-aliases-bulk': 'application/json', - 'admin/emoji/set-aliases-bulk': 'application/json', - 'admin/emoji/set-category-bulk': 'application/json', - 'admin/emoji/set-license-bulk': 'application/json', - 'admin/emoji/steal': 'application/json', - 'admin/emoji/update': 'application/json', - 'admin/federation/delete-all-files': 'application/json', - 'admin/federation/refresh-remote-instance-metadata': 'application/json', - 'admin/federation/remove-all-following': 'application/json', - 'admin/federation/update-instance': 'application/json', - 'admin/get-index-stats': 'application/json', - 'admin/get-table-stats': 'application/json', - 'admin/get-user-ips': 'application/json', - 'admin/invite/create': 'application/json', - 'admin/invite/list': 'application/json', - 'admin/invite/revoke': 'application/json', - 'admin/promo/create': 'application/json', - 'admin/queue/clear': 'application/json', - 'admin/queue/deliver-delayed': 'application/json', - 'admin/queue/inbox-delayed': 'application/json', - 'admin/queue/promote': 'application/json', - 'admin/queue/stats': 'application/json', - 'admin/relays/add': 'application/json', - 'admin/relays/list': 'application/json', - 'admin/relays/remove': 'application/json', - 'admin/reset-password': 'application/json', - 'admin/resolve-abuse-user-report': 'application/json', - 'admin/send-email': 'application/json', - 'admin/server-info': 'application/json', - 'admin/show-moderation-logs': 'application/json', - 'admin/show-user': 'application/json', - 'admin/show-users': 'application/json', - 'admin/suspend-user': 'application/json', - 'admin/unsuspend-user': 'application/json', - 'admin/set-user-sensitive': 'application/json', - 'admin/unset-user-sensitive': 'application/json', - 'admin/update-meta': 'application/json', - 'admin/delete-account': 'application/json', - 'admin/update-user-note': 'application/json', - 'admin/roles/create': 'application/json', - 'admin/roles/delete': 'application/json', - 'admin/roles/list': 'application/json', - 'admin/roles/show': 'application/json', - 'admin/roles/update': 'application/json', - 'admin/roles/assign': 'application/json', - 'admin/roles/unassign': 'application/json', - 'admin/roles/update-default-policies': 'application/json', - 'admin/roles/users': 'application/json', - 'admin/system-webhook/create': 'application/json', - 'admin/system-webhook/delete': 'application/json', - 'admin/system-webhook/list': 'application/json', - 'admin/system-webhook/show': 'application/json', - 'admin/system-webhook/update': 'application/json', - 'announcements': 'application/json', - 'announcements/show': 'application/json', - 'antennas/create': 'application/json', - 'antennas/delete': 'application/json', - 'antennas/list': 'application/json', - 'antennas/notes': 'application/json', - 'antennas/show': 'application/json', - 'antennas/update': 'application/json', - 'ap/get': 'application/json', - 'ap/show': 'application/json', - 'app/create': 'application/json', - 'app/show': 'application/json', - 'auth/accept': 'application/json', - 'auth/session/generate': 'application/json', - 'auth/session/show': 'application/json', - 'auth/session/userkey': 'application/json', - 'blocking/create': 'application/json', - 'blocking/delete': 'application/json', - 'blocking/list': 'application/json', - 'channels/create': 'application/json', - 'channels/featured': 'application/json', - 'channels/follow': 'application/json', - 'channels/followed': 'application/json', - 'channels/owned': 'application/json', - 'channels/show': 'application/json', - 'channels/timeline': 'application/json', - 'channels/unfollow': 'application/json', - 'channels/update': 'application/json', - 'channels/favorite': 'application/json', - 'channels/unfavorite': 'application/json', - 'channels/my-favorites': 'application/json', - 'channels/search': 'application/json', - 'charts/active-users': 'application/json', - 'charts/ap-request': 'application/json', - 'charts/drive': 'application/json', - 'charts/federation': 'application/json', - 'charts/instance': 'application/json', - 'charts/notes': 'application/json', - 'charts/user/drive': 'application/json', - 'charts/user/following': 'application/json', - 'charts/user/notes': 'application/json', - 'charts/user/pv': 'application/json', - 'charts/user/reactions': 'application/json', - 'charts/users': 'application/json', - 'clips/add-note': 'application/json', - 'clips/remove-note': 'application/json', - 'clips/create': 'application/json', - 'clips/delete': 'application/json', - 'clips/list': 'application/json', - 'clips/notes': 'application/json', - 'clips/show': 'application/json', - 'clips/update': 'application/json', - 'clips/favorite': 'application/json', - 'clips/unfavorite': 'application/json', - 'clips/my-favorites': 'application/json', - 'drive': 'application/json', - 'drive/files': 'application/json', - 'drive/files/attached-notes': 'application/json', - 'drive/files/check-existence': 'application/json', - 'drive/files/create': 'multipart/form-data', - 'drive/files/delete': 'application/json', - 'drive/files/find-by-hash': 'application/json', - 'drive/files/find': 'application/json', - 'drive/files/show': 'application/json', - 'drive/files/update': 'application/json', - 'drive/files/upload-from-url': 'application/json', - 'drive/folders': 'application/json', - 'drive/folders/create': 'application/json', - 'drive/folders/delete': 'application/json', - 'drive/folders/find': 'application/json', - 'drive/folders/show': 'application/json', - 'drive/folders/update': 'application/json', - 'drive/stream': 'application/json', - 'email-address/available': 'application/json', - 'endpoint': 'application/json', - 'endpoints': 'application/json', - 'export-custom-emojis': 'application/json', - 'federation/followers': 'application/json', - 'federation/following': 'application/json', - 'federation/instances': 'application/json', - 'federation/show-instance': 'application/json', - 'federation/update-remote-user': 'application/json', - 'federation/users': 'application/json', - 'federation/stats': 'application/json', - 'following/create': 'application/json', - 'following/delete': 'application/json', - 'following/update': 'application/json', - 'following/update-all': 'application/json', - 'following/invalidate': 'application/json', - 'following/requests/accept': 'application/json', - 'following/requests/cancel': 'application/json', - 'following/requests/list': 'application/json', - 'following/requests/reject': 'application/json', - 'gallery/featured': 'application/json', - 'gallery/popular': 'application/json', - 'gallery/posts': 'application/json', - 'gallery/posts/create': 'application/json', - 'gallery/posts/delete': 'application/json', - 'gallery/posts/like': 'application/json', - 'gallery/posts/show': 'application/json', - 'gallery/posts/unlike': 'application/json', - 'gallery/posts/update': 'application/json', - 'get-online-users-count': 'application/json', - 'get-avatar-decorations': 'application/json', - 'hashtags/list': 'application/json', - 'hashtags/search': 'application/json', - 'hashtags/show': 'application/json', - 'hashtags/trend': 'application/json', - 'hashtags/users': 'application/json', - 'i': 'application/json', - 'i/2fa/done': 'application/json', - 'i/2fa/key-done': 'application/json', - 'i/2fa/password-less': 'application/json', - 'i/2fa/register-key': 'application/json', - 'i/2fa/register': 'application/json', - 'i/2fa/update-key': 'application/json', - 'i/2fa/remove-key': 'application/json', - 'i/2fa/unregister': 'application/json', - 'i/apps': 'application/json', - 'i/authorized-apps': 'application/json', - 'i/claim-achievement': 'application/json', - 'i/change-password': 'application/json', - 'i/delete-account': 'application/json', - 'i/export-blocking': 'application/json', - 'i/export-following': 'application/json', - 'i/export-mute': 'application/json', - 'i/export-notes': 'application/json', - 'i/export-clips': 'application/json', - 'i/export-favorites': 'application/json', - 'i/export-user-lists': 'application/json', - 'i/export-antennas': 'application/json', - 'i/favorites': 'application/json', - 'i/gallery/likes': 'application/json', - 'i/gallery/posts': 'application/json', - 'i/import-blocking': 'application/json', - 'i/import-following': 'application/json', - 'i/import-muting': 'application/json', - 'i/import-user-lists': 'application/json', - 'i/import-antennas': 'application/json', - 'i/notifications': 'application/json', - 'i/notifications-grouped': 'application/json', - 'i/page-likes': 'application/json', - 'i/pages': 'application/json', - 'i/pin': 'application/json', - 'i/read-all-messaging-messages': 'application/json', - 'i/read-all-unread-notes': 'application/json', - 'i/read-announcement': 'application/json', - 'i/regenerate-token': 'application/json', - 'i/registry/get-all': 'application/json', - 'i/registry/get-detail': 'application/json', - 'i/registry/get': 'application/json', - 'i/registry/keys-with-type': 'application/json', - 'i/registry/keys': 'application/json', - 'i/registry/remove': 'application/json', - 'i/registry/scopes-with-domain': 'application/json', - 'i/registry/set': 'application/json', - 'i/revoke-token': 'application/json', - 'i/signin-history': 'application/json', - 'i/unpin': 'application/json', - 'i/update-email': 'application/json', - 'i/update': 'application/json', - 'i/user-group-invites': 'application/json', - 'i/move': 'application/json', - 'i/webhooks/create': 'application/json', - 'i/webhooks/list': 'application/json', - 'i/webhooks/show': 'application/json', - 'i/webhooks/update': 'application/json', - 'i/webhooks/delete': 'application/json', - 'invite/create': 'application/json', - 'invite/delete': 'application/json', - 'invite/list': 'application/json', - 'invite/limit': 'application/json', - 'messaging/history': 'application/json', - 'messaging/messages': 'application/json', - 'messaging/messages/create': 'application/json', - 'messaging/messages/delete': 'application/json', - 'messaging/messages/read': 'application/json', - 'meta': 'application/json', - 'emojis': 'application/json', - 'emoji': 'application/json', - 'miauth/gen-token': 'application/json', - 'mute/create': 'application/json', - 'mute/delete': 'application/json', - 'mute/list': 'application/json', - 'renote-mute/create': 'application/json', - 'renote-mute/delete': 'application/json', - 'renote-mute/list': 'application/json', - 'my/apps': 'application/json', - 'notes': 'application/json', - 'notes/children': 'application/json', - 'notes/clips': 'application/json', - 'notes/conversation': 'application/json', - 'notes/create': 'application/json', - 'notes/delete': 'application/json', - 'notes/update': 'application/json', - 'notes/favorites/create': 'application/json', - 'notes/favorites/delete': 'application/json', - 'notes/featured': 'application/json', - 'notes/global-timeline': 'application/json', - 'notes/hybrid-timeline': 'application/json', - 'notes/local-timeline': 'application/json', - 'notes/mentions': 'application/json', - 'notes/polls/recommendation': 'application/json', - 'notes/polls/vote': 'application/json', - 'notes/events/search': 'application/json', - 'notes/reactions': 'application/json', - 'notes/reactions/create': 'application/json', - 'notes/reactions/delete': 'application/json', - 'notes/renotes': 'application/json', - 'notes/replies': 'application/json', - 'notes/search-by-tag': 'application/json', - 'notes/search': 'application/json', - 'notes/show': 'application/json', - 'notes/state': 'application/json', - 'notes/thread-muting/create': 'application/json', - 'notes/thread-muting/delete': 'application/json', - 'notes/timeline': 'application/json', - 'notes/translate': 'application/json', - 'notes/unrenote': 'application/json', - 'notes/user-list-timeline': 'application/json', - 'notifications/create': 'application/json', - 'notifications/delete': 'application/json', - 'notifications/flush': 'application/json', - 'notifications/mark-all-as-read': 'application/json', - 'notifications/test-notification': 'application/json', - 'page-push': 'application/json', - 'pages/create': 'application/json', - 'pages/delete': 'application/json', - 'pages/featured': 'application/json', - 'pages/like': 'application/json', - 'pages/show': 'application/json', - 'pages/unlike': 'application/json', - 'pages/update': 'application/json', - 'flash/create': 'application/json', - 'flash/delete': 'application/json', - 'flash/featured': 'application/json', - 'flash/gen-token': 'application/json', - 'flash/like': 'application/json', - 'flash/show': 'application/json', - 'flash/unlike': 'application/json', - 'flash/update': 'application/json', - 'flash/my': 'application/json', - 'flash/my-likes': 'application/json', - 'ping': 'application/json', - 'pinned-users': 'application/json', - 'promo/read': 'application/json', - 'roles/list': 'application/json', - 'roles/show': 'application/json', - 'roles/users': 'application/json', - 'roles/notes': 'application/json', - 'request-reset-password': 'application/json', - 'reset-db': 'application/json', - 'reset-password': 'application/json', - 'server-info': 'application/json', - 'stats': 'application/json', - 'sw/show-registration': 'application/json', - 'sw/update-registration': 'application/json', - 'sw/register': 'application/json', - 'sw/unregister': 'application/json', - 'test': 'application/json', - 'username/available': 'application/json', - 'users': 'application/json', - 'users/clips': 'application/json', - 'users/followers': 'application/json', - 'users/following': 'application/json', - 'users/gallery/posts': 'application/json', - 'users/get-frequently-replied-users': 'application/json', - 'users/featured-notes': 'application/json', - 'users/groups/create': 'application/json', - 'users/groups/delete': 'application/json', - 'users/groups/invitations/accept': 'application/json', - 'users/groups/invitations/reject': 'application/json', - 'users/groups/invite': 'application/json', - 'users/groups/joined': 'application/json', - 'users/groups/leave': 'application/json', - 'users/groups/owned': 'application/json', - 'users/groups/pull': 'application/json', - 'users/groups/show': 'application/json', - 'users/groups/transfer': 'application/json', - 'users/groups/update': 'application/json', - 'users/lists/create': 'application/json', - 'users/lists/delete': 'application/json', - 'users/lists/list': 'application/json', - 'users/lists/pull': 'application/json', - 'users/lists/push': 'application/json', - 'users/lists/show': 'application/json', - 'users/lists/favorite': 'application/json', - 'users/lists/unfavorite': 'application/json', - 'users/lists/update': 'application/json', - 'users/lists/create-from-public': 'application/json', - 'users/lists/update-membership': 'application/json', - 'users/lists/get-memberships': 'application/json', - 'users/notes': 'application/json', - 'users/pages': 'application/json', - 'users/flashs': 'application/json', - 'users/reactions': 'application/json', - 'users/recommendation': 'application/json', - 'users/relation': 'application/json', - 'users/report-abuse': 'application/json', - 'users/search-by-username-and-host': 'application/json', - 'users/search': 'application/json', - 'users/show': 'application/json', - 'users/stats': 'application/json', - 'users/achievements': 'application/json', - 'users/update-memo': 'application/json', - 'users/translate': 'application/json', - 'fetch-rss': 'application/json', - 'fetch-external-resources': 'application/json', - 'retention': 'application/json', - 'bubble-game/register': 'application/json', - 'bubble-game/ranking': 'application/json', -}; diff --git a/packages/cherrypick-js/src/autogen/entities.ts b/packages/cherrypick-js/src/autogen/entities.ts index a94d4ca9ee..765e0ce971 100644 --- a/packages/cherrypick-js/src/autogen/entities.ts +++ b/packages/cherrypick-js/src/autogen/entities.ts @@ -1,613 +1,589 @@ -/* eslint @typescript-eslint/naming-convention: 0 */ +/* + * version: 4.6.0 + * basedMisskeyVersion: 2023.12.2 + * generatedAt: 2024-01-08T10:34:58.481Z + */ + import { operations } from './types.js'; export type EmptyRequest = Record | undefined; export type EmptyResponse = Record | undefined; -export type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json']; -export type AdminAbuseReportResolverCreateRequest = operations['admin___abuse-report-resolver___create']['requestBody']['content']['application/json']; -export type AdminAbuseReportResolverCreateResponse = operations['admin___abuse-report-resolver___create']['responses']['200']['content']['application/json']; -export type AdminAbuseReportResolverListRequest = operations['admin___abuse-report-resolver___list']['requestBody']['content']['application/json']; -export type AdminAbuseReportResolverListResponse = operations['admin___abuse-report-resolver___list']['responses']['200']['content']['application/json']; -export type AdminAbuseReportResolverDeleteRequest = operations['admin___abuse-report-resolver___delete']['requestBody']['content']['application/json']; -export type AdminAbuseReportResolverUpdateRequest = operations['admin___abuse-report-resolver___update']['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 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 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']; -export type AdminAccountsFindByEmailRequest = operations['admin___accounts___find-by-email']['requestBody']['content']['application/json']; -export type AdminAccountsFindByEmailResponse = operations['admin___accounts___find-by-email']['responses']['200']['content']['application/json']; -export type AdminAdCreateRequest = operations['admin___ad___create']['requestBody']['content']['application/json']; -export type AdminAdCreateResponse = operations['admin___ad___create']['responses']['200']['content']['application/json']; -export type AdminAdDeleteRequest = operations['admin___ad___delete']['requestBody']['content']['application/json']; -export type AdminAdListRequest = operations['admin___ad___list']['requestBody']['content']['application/json']; -export type AdminAdListResponse = operations['admin___ad___list']['responses']['200']['content']['application/json']; -export type AdminAdUpdateRequest = operations['admin___ad___update']['requestBody']['content']['application/json']; -export type AdminAnnouncementsCreateRequest = operations['admin___announcements___create']['requestBody']['content']['application/json']; -export type AdminAnnouncementsCreateResponse = operations['admin___announcements___create']['responses']['200']['content']['application/json']; -export type AdminAnnouncementsDeleteRequest = operations['admin___announcements___delete']['requestBody']['content']['application/json']; -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 AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['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 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 AdminEmojiAddsRequest = operations['admin___emoji___adds']['requestBody']['content']['application/json']; -export type AdminEmojiAddsResponse = operations['admin___emoji___adds']['responses']['200']['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 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 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']; -export type AdminEmojiSetLicenseBulkRequest = operations['admin___emoji___set-license-bulk']['requestBody']['content']['application/json']; -export type AdminEmojiStealRequest = operations['admin___emoji___steal']['requestBody']['content']['application/json']; -export type AdminEmojiStealResponse = operations['admin___emoji___steal']['responses']['200']['content']['application/json']; -export type AdminEmojiUpdateRequest = operations['admin___emoji___update']['requestBody']['content']['application/json']; -export type AdminFederationDeleteAllFilesRequest = operations['admin___federation___delete-all-files']['requestBody']['content']['application/json']; -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 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']; -export type AdminGetUserIpsResponse = operations['admin___get-user-ips']['responses']['200']['content']['application/json']; -export type AdminInviteCreateRequest = operations['admin___invite___create']['requestBody']['content']['application/json']; -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 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 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']; -export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['requestBody']['content']['application/json']; -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 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 AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json']; -export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; -export type AdminSetUserSensitiveRequest = operations['admin___set-user-sensitive']['requestBody']['content']['application/json']; -export type AdminUnsetUserSensitiveRequest = operations['admin___unset-user-sensitive']['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 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 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 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']; -export type AdminSystemWebhookListRequest = operations['admin___system-webhook___list']['requestBody']['content']['application/json']; -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 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 AdminMetaResponse = operations['admin/meta']['responses']['200']['content']['application/json']; +export type AdminAbuseReportResolverCreateRequest = operations['admin/abuse-report-resolver/create']['requestBody']['content']['application/json']; +export type AdminAbuseReportResolverCreateResponse = operations['admin/abuse-report-resolver/create']['responses']['200']['content']['application/json']; +export type AdminAbuseReportResolverListRequest = operations['admin/abuse-report-resolver/list']['requestBody']['content']['application/json']; +export type AdminAbuseReportResolverListResponse = operations['admin/abuse-report-resolver/list']['responses']['200']['content']['application/json']; +export type AdminAbuseReportResolverDeleteRequest = operations['admin/abuse-report-resolver/delete']['requestBody']['content']['application/json']; +export type AdminAbuseReportResolverUpdateRequest = operations['admin/abuse-report-resolver/update']['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']; +export type AdminAccountsFindByEmailRequest = operations['admin/accounts/find-by-email']['requestBody']['content']['application/json']; +export type AdminAccountsFindByEmailResponse = operations['admin/accounts/find-by-email']['responses']['200']['content']['application/json']; +export type AdminAdCreateRequest = operations['admin/ad/create']['requestBody']['content']['application/json']; +export type AdminAdCreateResponse = operations['admin/ad/create']['responses']['200']['content']['application/json']; +export type AdminAdDeleteRequest = operations['admin/ad/delete']['requestBody']['content']['application/json']; +export type AdminAdListRequest = operations['admin/ad/list']['requestBody']['content']['application/json']; +export type AdminAdListResponse = operations['admin/ad/list']['responses']['200']['content']['application/json']; +export type AdminAdUpdateRequest = operations['admin/ad/update']['requestBody']['content']['application/json']; +export type AdminAnnouncementsCreateRequest = operations['admin/announcements/create']['requestBody']['content']['application/json']; +export type AdminAnnouncementsCreateResponse = operations['admin/announcements/create']['responses']['200']['content']['application/json']; +export type AdminAnnouncementsDeleteRequest = operations['admin/announcements/delete']['requestBody']['content']['application/json']; +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 AdminAvatarDecorationsCreateRequest = operations['admin/avatar-decorations/create']['requestBody']['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 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 AdminEmojiAddsRequest = operations['admin/emoji/adds']['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 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 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']; +export type AdminEmojiSetLicenseBulkRequest = operations['admin/emoji/set-license-bulk']['requestBody']['content']['application/json']; +export type AdminEmojiStealRequest = operations['admin/emoji/steal']['requestBody']['content']['application/json']; +export type AdminEmojiStealResponse = operations['admin/emoji/steal']['responses']['200']['content']['application/json']; +export type AdminEmojiUpdateRequest = operations['admin/emoji/update']['requestBody']['content']['application/json']; +export type AdminFederationDeleteAllFilesRequest = operations['admin/federation/delete-all-files']['requestBody']['content']['application/json']; +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 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']; +export type AdminGetUserIpsResponse = operations['admin/get-user-ips']['responses']['200']['content']['application/json']; +export type AdminInviteCreateRequest = operations['admin/invite/create']['requestBody']['content']['application/json']; +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 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 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']; +export type AdminRelaysRemoveRequest = operations['admin/relays/remove']['requestBody']['content']['application/json']; +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 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 AdminSuspendUserRequest = operations['admin/suspend-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 AdminDeleteAccountResponse = operations['admin/delete-account']['responses']['200']['content']['application/json']; +export type AdminUpdateUserNoteRequest = operations['admin/update-user-note']['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 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 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']; -export type AnnouncementsShowResponse = operations['announcements___show']['responses']['200']['content']['application/json']; -export type AntennasCreateRequest = operations['antennas___create']['requestBody']['content']['application/json']; -export type AntennasCreateResponse = operations['antennas___create']['responses']['200']['content']['application/json']; -export type AntennasDeleteRequest = operations['antennas___delete']['requestBody']['content']['application/json']; -export type AntennasListResponse = operations['antennas___list']['responses']['200']['content']['application/json']; -export type AntennasNotesRequest = operations['antennas___notes']['requestBody']['content']['application/json']; -export type AntennasNotesResponse = operations['antennas___notes']['responses']['200']['content']['application/json']; -export type AntennasShowRequest = operations['antennas___show']['requestBody']['content']['application/json']; -export type AntennasShowResponse = operations['antennas___show']['responses']['200']['content']['application/json']; -export type AntennasUpdateRequest = operations['antennas___update']['requestBody']['content']['application/json']; -export type AntennasUpdateResponse = operations['antennas___update']['responses']['200']['content']['application/json']; -export type ApGetRequest = operations['ap___get']['requestBody']['content']['application/json']; -export type ApGetResponse = operations['ap___get']['responses']['200']['content']['application/json']; -export type ApShowRequest = operations['ap___show']['requestBody']['content']['application/json']; -export type ApShowResponse = operations['ap___show']['responses']['200']['content']['application/json']; -export type AppCreateRequest = operations['app___create']['requestBody']['content']['application/json']; -export type AppCreateResponse = operations['app___create']['responses']['200']['content']['application/json']; -export type AppShowRequest = operations['app___show']['requestBody']['content']['application/json']; -export type AppShowResponse = operations['app___show']['responses']['200']['content']['application/json']; -export type AuthAcceptRequest = operations['auth___accept']['requestBody']['content']['application/json']; -export type AuthSessionGenerateRequest = operations['auth___session___generate']['requestBody']['content']['application/json']; -export type AuthSessionGenerateResponse = operations['auth___session___generate']['responses']['200']['content']['application/json']; -export type AuthSessionShowRequest = operations['auth___session___show']['requestBody']['content']['application/json']; -export type AuthSessionShowResponse = operations['auth___session___show']['responses']['200']['content']['application/json']; -export type AuthSessionUserkeyRequest = operations['auth___session___userkey']['requestBody']['content']['application/json']; -export type AuthSessionUserkeyResponse = operations['auth___session___userkey']['responses']['200']['content']['application/json']; -export type BlockingCreateRequest = operations['blocking___create']['requestBody']['content']['application/json']; -export type BlockingCreateResponse = operations['blocking___create']['responses']['200']['content']['application/json']; -export type BlockingDeleteRequest = operations['blocking___delete']['requestBody']['content']['application/json']; -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 ChannelsCreateRequest = operations['channels___create']['requestBody']['content']['application/json']; -export type ChannelsCreateResponse = operations['channels___create']['responses']['200']['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 ChannelsOwnedRequest = operations['channels___owned']['requestBody']['content']['application/json']; -export type ChannelsOwnedResponse = operations['channels___owned']['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 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']; -export type ChartsApRequestResponse = operations['charts___ap-request']['responses']['200']['content']['application/json']; -export type ChartsDriveRequest = operations['charts___drive']['requestBody']['content']['application/json']; -export type ChartsDriveResponse = operations['charts___drive']['responses']['200']['content']['application/json']; -export type ChartsFederationRequest = operations['charts___federation']['requestBody']['content']['application/json']; -export type ChartsFederationResponse = operations['charts___federation']['responses']['200']['content']['application/json']; -export type ChartsInstanceRequest = operations['charts___instance']['requestBody']['content']['application/json']; -export type ChartsInstanceResponse = operations['charts___instance']['responses']['200']['content']['application/json']; -export type ChartsNotesRequest = operations['charts___notes']['requestBody']['content']['application/json']; -export type ChartsNotesResponse = operations['charts___notes']['responses']['200']['content']['application/json']; -export type ChartsUserDriveRequest = operations['charts___user___drive']['requestBody']['content']['application/json']; -export type ChartsUserDriveResponse = operations['charts___user___drive']['responses']['200']['content']['application/json']; -export type ChartsUserFollowingRequest = operations['charts___user___following']['requestBody']['content']['application/json']; -export type ChartsUserFollowingResponse = operations['charts___user___following']['responses']['200']['content']['application/json']; -export type ChartsUserNotesRequest = operations['charts___user___notes']['requestBody']['content']['application/json']; -export type ChartsUserNotesResponse = operations['charts___user___notes']['responses']['200']['content']['application/json']; -export type ChartsUserPvRequest = operations['charts___user___pv']['requestBody']['content']['application/json']; -export type ChartsUserPvResponse = operations['charts___user___pv']['responses']['200']['content']['application/json']; -export type ChartsUserReactionsRequest = operations['charts___user___reactions']['requestBody']['content']['application/json']; -export type ChartsUserReactionsResponse = operations['charts___user___reactions']['responses']['200']['content']['application/json']; -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 ClipsListResponse = operations['clips___list']['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 ClipsShowRequest = operations['clips___show']['requestBody']['content']['application/json']; -export type ClipsShowResponse = operations['clips___show']['responses']['200']['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 AntennasCreateRequest = operations['antennas/create']['requestBody']['content']['application/json']; +export type AntennasCreateResponse = operations['antennas/create']['responses']['200']['content']['application/json']; +export type AntennasDeleteRequest = operations['antennas/delete']['requestBody']['content']['application/json']; +export type AntennasListResponse = operations['antennas/list']['responses']['200']['content']['application/json']; +export type AntennasNotesRequest = operations['antennas/notes']['requestBody']['content']['application/json']; +export type AntennasNotesResponse = operations['antennas/notes']['responses']['200']['content']['application/json']; +export type AntennasShowRequest = operations['antennas/show']['requestBody']['content']['application/json']; +export type AntennasShowResponse = operations['antennas/show']['responses']['200']['content']['application/json']; +export type AntennasUpdateRequest = operations['antennas/update']['requestBody']['content']['application/json']; +export type AntennasUpdateResponse = operations['antennas/update']['responses']['200']['content']['application/json']; +export type ApGetRequest = operations['ap/get']['requestBody']['content']['application/json']; +export type ApGetResponse = operations['ap/get']['responses']['200']['content']['application/json']; +export type ApShowRequest = operations['ap/show']['requestBody']['content']['application/json']; +export type ApShowResponse = operations['ap/show']['responses']['200']['content']['application/json']; +export type AppCreateRequest = operations['app/create']['requestBody']['content']['application/json']; +export type AppCreateResponse = operations['app/create']['responses']['200']['content']['application/json']; +export type AppShowRequest = operations['app/show']['requestBody']['content']['application/json']; +export type AppShowResponse = operations['app/show']['responses']['200']['content']['application/json']; +export type AuthAcceptRequest = operations['auth/accept']['requestBody']['content']['application/json']; +export type AuthSessionGenerateRequest = operations['auth/session/generate']['requestBody']['content']['application/json']; +export type AuthSessionGenerateResponse = operations['auth/session/generate']['responses']['200']['content']['application/json']; +export type AuthSessionShowRequest = operations['auth/session/show']['requestBody']['content']['application/json']; +export type AuthSessionShowResponse = operations['auth/session/show']['responses']['200']['content']['application/json']; +export type AuthSessionUserkeyRequest = operations['auth/session/userkey']['requestBody']['content']['application/json']; +export type AuthSessionUserkeyResponse = operations['auth/session/userkey']['responses']['200']['content']['application/json']; +export type BlockingCreateRequest = operations['blocking/create']['requestBody']['content']['application/json']; +export type BlockingCreateResponse = operations['blocking/create']['responses']['200']['content']['application/json']; +export type BlockingDeleteRequest = operations['blocking/delete']['requestBody']['content']['application/json']; +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 ChannelsCreateRequest = operations['channels/create']['requestBody']['content']['application/json']; +export type ChannelsCreateResponse = operations['channels/create']['responses']['200']['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 ChannelsOwnedRequest = operations['channels/owned']['requestBody']['content']['application/json']; +export type ChannelsOwnedResponse = operations['channels/owned']['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 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']; +export type ChartsApRequestResponse = operations['charts/ap-request']['responses']['200']['content']['application/json']; +export type ChartsDriveRequest = operations['charts/drive']['requestBody']['content']['application/json']; +export type ChartsDriveResponse = operations['charts/drive']['responses']['200']['content']['application/json']; +export type ChartsFederationRequest = operations['charts/federation']['requestBody']['content']['application/json']; +export type ChartsFederationResponse = operations['charts/federation']['responses']['200']['content']['application/json']; +export type ChartsInstanceRequest = operations['charts/instance']['requestBody']['content']['application/json']; +export type ChartsInstanceResponse = operations['charts/instance']['responses']['200']['content']['application/json']; +export type ChartsNotesRequest = operations['charts/notes']['requestBody']['content']['application/json']; +export type ChartsNotesResponse = operations['charts/notes']['responses']['200']['content']['application/json']; +export type ChartsUserDriveRequest = operations['charts/user/drive']['requestBody']['content']['application/json']; +export type ChartsUserDriveResponse = operations['charts/user/drive']['responses']['200']['content']['application/json']; +export type ChartsUserFollowingRequest = operations['charts/user/following']['requestBody']['content']['application/json']; +export type ChartsUserFollowingResponse = operations['charts/user/following']['responses']['200']['content']['application/json']; +export type ChartsUserNotesRequest = operations['charts/user/notes']['requestBody']['content']['application/json']; +export type ChartsUserNotesResponse = operations['charts/user/notes']['responses']['200']['content']['application/json']; +export type ChartsUserPvRequest = operations['charts/user/pv']['requestBody']['content']['application/json']; +export type ChartsUserPvResponse = operations['charts/user/pv']['responses']['200']['content']['application/json']; +export type ChartsUserReactionsRequest = operations['charts/user/reactions']['requestBody']['content']['application/json']; +export type ChartsUserReactionsResponse = operations['charts/user/reactions']['responses']['200']['content']['application/json']; +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 ClipsListResponse = operations['clips/list']['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 ClipsShowRequest = operations['clips/show']['requestBody']['content']['application/json']; +export type ClipsShowResponse = operations['clips/show']['responses']['200']['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']; -export type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json']; -export type DriveFilesAttachedNotesResponse = operations['drive___files___attached-notes']['responses']['200']['content']['application/json']; -export type DriveFilesCheckExistenceRequest = operations['drive___files___check-existence']['requestBody']['content']['application/json']; -export type DriveFilesCheckExistenceResponse = operations['drive___files___check-existence']['responses']['200']['content']['application/json']; -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 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']; -export type DriveFilesUpdateResponse = operations['drive___files___update']['responses']['200']['content']['application/json']; -export type DriveFilesUploadFromUrlRequest = operations['drive___files___upload-from-url']['requestBody']['content']['application/json']; -export type DriveFoldersRequest = operations['drive___folders']['requestBody']['content']['application/json']; -export type DriveFoldersResponse = operations['drive___folders']['responses']['200']['content']['application/json']; -export type DriveFoldersCreateRequest = operations['drive___folders___create']['requestBody']['content']['application/json']; -export type DriveFoldersCreateResponse = operations['drive___folders___create']['responses']['200']['content']['application/json']; -export type DriveFoldersDeleteRequest = operations['drive___folders___delete']['requestBody']['content']['application/json']; -export type DriveFoldersFindRequest = operations['drive___folders___find']['requestBody']['content']['application/json']; -export type DriveFoldersFindResponse = operations['drive___folders___find']['responses']['200']['content']['application/json']; -export type DriveFoldersShowRequest = operations['drive___folders___show']['requestBody']['content']['application/json']; -export type DriveFoldersShowResponse = operations['drive___folders___show']['responses']['200']['content']['application/json']; -export type DriveFoldersUpdateRequest = operations['drive___folders___update']['requestBody']['content']['application/json']; -export type DriveFoldersUpdateResponse = operations['drive___folders___update']['responses']['200']['content']['application/json']; -export type DriveStreamRequest = operations['drive___stream']['requestBody']['content']['application/json']; -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 DriveFilesRequest = operations['drive/files']['requestBody']['content']['application/json']; +export type DriveFilesResponse = operations['drive/files']['responses']['200']['content']['application/json']; +export type DriveFilesAttachedNotesRequest = operations['drive/files/attached-notes']['requestBody']['content']['application/json']; +export type DriveFilesAttachedNotesResponse = operations['drive/files/attached-notes']['responses']['200']['content']['application/json']; +export type DriveFilesCheckExistenceRequest = operations['drive/files/check-existence']['requestBody']['content']['application/json']; +export type DriveFilesCheckExistenceResponse = operations['drive/files/check-existence']['responses']['200']['content']['application/json']; +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 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']; +export type DriveFilesUpdateResponse = operations['drive/files/update']['responses']['200']['content']['application/json']; +export type DriveFilesUploadFromUrlRequest = operations['drive/files/upload-from-url']['requestBody']['content']['application/json']; +export type DriveFoldersRequest = operations['drive/folders']['requestBody']['content']['application/json']; +export type DriveFoldersResponse = operations['drive/folders']['responses']['200']['content']['application/json']; +export type DriveFoldersCreateRequest = operations['drive/folders/create']['requestBody']['content']['application/json']; +export type DriveFoldersCreateResponse = operations['drive/folders/create']['responses']['200']['content']['application/json']; +export type DriveFoldersDeleteRequest = operations['drive/folders/delete']['requestBody']['content']['application/json']; +export type DriveFoldersFindRequest = operations['drive/folders/find']['requestBody']['content']['application/json']; +export type DriveFoldersFindResponse = operations['drive/folders/find']['responses']['200']['content']['application/json']; +export type DriveFoldersShowRequest = operations['drive/folders/show']['requestBody']['content']['application/json']; +export type DriveFoldersShowResponse = operations['drive/folders/show']['responses']['200']['content']['application/json']; +export type DriveFoldersUpdateRequest = operations['drive/folders/update']['requestBody']['content']['application/json']; +export type DriveFoldersUpdateResponse = operations['drive/folders/update']['responses']['200']['content']['application/json']; +export type DriveStreamRequest = operations['drive/stream']['requestBody']['content']['application/json']; +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 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']; -export type FederationFollowersRequest = operations['federation___followers']['requestBody']['content']['application/json']; -export type FederationFollowersResponse = operations['federation___followers']['responses']['200']['content']['application/json']; -export type FederationFollowingRequest = operations['federation___following']['requestBody']['content']['application/json']; -export type FederationFollowingResponse = operations['federation___following']['responses']['200']['content']['application/json']; -export type FederationInstancesRequest = operations['federation___instances']['requestBody']['content']['application/json']; -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 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 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']; -export type FollowingRequestsCancelRequest = operations['following___requests___cancel']['requestBody']['content']['application/json']; -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 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']; -export type GalleryPostsRequest = operations['gallery___posts']['requestBody']['content']['application/json']; -export type GalleryPostsResponse = operations['gallery___posts']['responses']['200']['content']['application/json']; -export type GalleryPostsCreateRequest = operations['gallery___posts___create']['requestBody']['content']['application/json']; -export type GalleryPostsCreateResponse = operations['gallery___posts___create']['responses']['200']['content']['application/json']; -export type GalleryPostsDeleteRequest = operations['gallery___posts___delete']['requestBody']['content']['application/json']; -export type GalleryPostsLikeRequest = operations['gallery___posts___like']['requestBody']['content']['application/json']; -export type GalleryPostsShowRequest = operations['gallery___posts___show']['requestBody']['content']['application/json']; -export type GalleryPostsShowResponse = operations['gallery___posts___show']['responses']['200']['content']['application/json']; -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 FederationFollowersRequest = operations['federation/followers']['requestBody']['content']['application/json']; +export type FederationFollowersResponse = operations['federation/followers']['responses']['200']['content']['application/json']; +export type FederationFollowingRequest = operations['federation/following']['requestBody']['content']['application/json']; +export type FederationFollowingResponse = operations['federation/following']['responses']['200']['content']['application/json']; +export type FederationInstancesRequest = operations['federation/instances']['requestBody']['content']['application/json']; +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 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 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']; +export type FollowingRequestsCancelRequest = operations['following/requests/cancel']['requestBody']['content']['application/json']; +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 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']; +export type GalleryPostsRequest = operations['gallery/posts']['requestBody']['content']['application/json']; +export type GalleryPostsResponse = operations['gallery/posts']['responses']['200']['content']['application/json']; +export type GalleryPostsCreateRequest = operations['gallery/posts/create']['requestBody']['content']['application/json']; +export type GalleryPostsCreateResponse = operations['gallery/posts/create']['responses']['200']['content']['application/json']; +export type GalleryPostsDeleteRequest = operations['gallery/posts/delete']['requestBody']['content']['application/json']; +export type GalleryPostsLikeRequest = operations['gallery/posts/like']['requestBody']['content']['application/json']; +export type GalleryPostsShowRequest = operations['gallery/posts/show']['requestBody']['content']['application/json']; +export type GalleryPostsShowResponse = operations['gallery/posts/show']['responses']['200']['content']['application/json']; +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 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']; -export type HashtagsSearchResponse = operations['hashtags___search']['responses']['200']['content']['application/json']; -export type HashtagsShowRequest = operations['hashtags___show']['requestBody']['content']['application/json']; -export type HashtagsShowResponse = operations['hashtags___show']['responses']['200']['content']['application/json']; -export type HashtagsTrendResponse = operations['hashtags___trend']['responses']['200']['content']['application/json']; -export type HashtagsUsersRequest = operations['hashtags___users']['requestBody']['content']['application/json']; -export type HashtagsUsersResponse = operations['hashtags___users']['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']; +export type HashtagsSearchResponse = operations['hashtags/search']['responses']['200']['content']['application/json']; +export type HashtagsShowRequest = operations['hashtags/show']['requestBody']['content']['application/json']; +export type HashtagsShowResponse = operations['hashtags/show']['responses']['200']['content']['application/json']; +export type HashtagsTrendResponse = operations['hashtags/trend']['responses']['200']['content']['application/json']; +export type HashtagsUsersRequest = operations['hashtags/users']['requestBody']['content']['application/json']; +export type HashtagsUsersResponse = operations['hashtags/users']['responses']['200']['content']['application/json']; export type IResponse = operations['i']['responses']['200']['content']['application/json']; -export type I2faDoneRequest = operations['i___2fa___done']['requestBody']['content']['application/json']; -export type I2faDoneResponse = operations['i___2fa___done']['responses']['200']['content']['application/json']; -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 I2faRemoveKeyRequest = operations['i___2fa___remove-key']['requestBody']['content']['application/json']; -export type I2faUnregisterRequest = operations['i___2fa___unregister']['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 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']; -export type IFavoritesResponse = operations['i___favorites']['responses']['200']['content']['application/json']; -export type IGalleryLikesRequest = operations['i___gallery___likes']['requestBody']['content']['application/json']; -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 IImportBlockingRequest = operations['i___import-blocking']['requestBody']['content']['application/json']; -export type IImportFollowingRequest = operations['i___import-following']['requestBody']['content']['application/json']; -export type IImportMutingRequest = operations['i___import-muting']['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 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']; -export type INotificationsGroupedResponse = operations['i___notifications-grouped']['responses']['200']['content']['application/json']; -export type IPageLikesRequest = operations['i___page-likes']['requestBody']['content']['application/json']; -export type IPageLikesResponse = operations['i___page-likes']['responses']['200']['content']['application/json']; -export type IPagesRequest = operations['i___pages']['requestBody']['content']['application/json']; -export type IPagesResponse = operations['i___pages']['responses']['200']['content']['application/json']; -export type IPinRequest = operations['i___pin']['requestBody']['content']['application/json']; -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 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 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 IRegistryKeysRequest = operations['i___registry___keys']['requestBody']['content']['application/json']; -export type IRegistryKeysResponse = operations['i___registry___keys']['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']; -export type IRevokeTokenRequest = operations['i___revoke-token']['requestBody']['content']['application/json']; -export type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['content']['application/json']; -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 IUserGroupInvitesRequest = operations['i___user-group-invites']['requestBody']['content']['application/json']; -export type IUserGroupInvitesResponse = operations['i___user-group-invites']['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 IWebhooksCreateRequest = operations['i___webhooks___create']['requestBody']['content']['application/json']; -export type IWebhooksCreateResponse = operations['i___webhooks___create']['responses']['200']['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 InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json']; -export type InviteDeleteRequest = operations['invite___delete']['requestBody']['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 MessagingHistoryRequest = operations['messaging___history']['requestBody']['content']['application/json']; -export type MessagingHistoryResponse = operations['messaging___history']['responses']['200']['content']['application/json']; -export type MessagingMessagesRequest = operations['messaging___messages']['requestBody']['content']['application/json']; -export type MessagingMessagesResponse = operations['messaging___messages']['responses']['200']['content']['application/json']; -export type MessagingMessagesCreateRequest = operations['messaging___messages___create']['requestBody']['content']['application/json']; -export type MessagingMessagesCreateResponse = operations['messaging___messages___create']['responses']['200']['content']['application/json']; -export type MessagingMessagesDeleteRequest = operations['messaging___messages___delete']['requestBody']['content']['application/json']; -export type MessagingMessagesReadRequest = operations['messaging___messages___read']['requestBody']['content']['application/json']; +export type I2faDoneRequest = operations['i/2fa/done']['requestBody']['content']['application/json']; +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 I2faRemoveKeyRequest = operations['i/2fa/remove-key']['requestBody']['content']['application/json']; +export type I2faUnregisterRequest = operations['i/2fa/unregister']['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 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']; +export type IFavoritesResponse = operations['i/favorites']['responses']['200']['content']['application/json']; +export type IGalleryLikesRequest = operations['i/gallery/likes']['requestBody']['content']['application/json']; +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 IImportBlockingRequest = operations['i/import-blocking']['requestBody']['content']['application/json']; +export type IImportFollowingRequest = operations['i/import-following']['requestBody']['content']['application/json']; +export type IImportMutingRequest = operations['i/import-muting']['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 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']; +export type INotificationsGroupedResponse = operations['i/notifications-grouped']['responses']['200']['content']['application/json']; +export type IPageLikesRequest = operations['i/page-likes']['requestBody']['content']['application/json']; +export type IPageLikesResponse = operations['i/page-likes']['responses']['200']['content']['application/json']; +export type IPagesRequest = operations['i/pages']['requestBody']['content']['application/json']; +export type IPagesResponse = operations['i/pages']['responses']['200']['content']['application/json']; +export type IPinRequest = operations['i/pin']['requestBody']['content']['application/json']; +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 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 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 IRegistryKeysRequest = operations['i/registry/keys']['requestBody']['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']; +export type IRevokeTokenRequest = operations['i/revoke-token']['requestBody']['content']['application/json']; +export type ISigninHistoryRequest = operations['i/signin-history']['requestBody']['content']['application/json']; +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 IUserGroupInvitesRequest = operations['i/user-group-invites']['requestBody']['content']['application/json']; +export type IUserGroupInvitesResponse = operations['i/user-group-invites']['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 IWebhooksCreateRequest = operations['i/webhooks/create']['requestBody']['content']['application/json']; +export type IWebhooksCreateResponse = operations['i/webhooks/create']['responses']['200']['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 InviteCreateResponse = operations['invite/create']['responses']['200']['content']['application/json']; +export type InviteDeleteRequest = operations['invite/delete']['requestBody']['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 MessagingHistoryRequest = operations['messaging/history']['requestBody']['content']['application/json']; +export type MessagingHistoryResponse = operations['messaging/history']['responses']['200']['content']['application/json']; +export type MessagingMessagesRequest = operations['messaging/messages']['requestBody']['content']['application/json']; +export type MessagingMessagesResponse = operations['messaging/messages']['responses']['200']['content']['application/json']; +export type MessagingMessagesCreateRequest = operations['messaging/messages/create']['requestBody']['content']['application/json']; +export type MessagingMessagesCreateResponse = operations['messaging/messages/create']['responses']['200']['content']['application/json']; +export type MessagingMessagesDeleteRequest = operations['messaging/messages/delete']['requestBody']['content']['application/json']; +export type MessagingMessagesReadRequest = operations['messaging/messages/read']['requestBody']['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 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 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']; -export type NotesClipsResponse = operations['notes___clips']['responses']['200']['content']['application/json']; -export type NotesConversationRequest = operations['notes___conversation']['requestBody']['content']['application/json']; -export type NotesConversationResponse = operations['notes___conversation']['responses']['200']['content']['application/json']; -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 NotesUpdateRequest = operations['notes___update']['requestBody']['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']; -export type NotesFeaturedResponse = operations['notes___featured']['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 NotesHybridTimelineRequest = operations['notes___hybrid-timeline']['requestBody']['content']['application/json']; -export type NotesHybridTimelineResponse = operations['notes___hybrid-timeline']['responses']['200']['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 NotesEventsSearchRequest = operations['notes___events___search']['requestBody']['content']['application/json']; -export type NotesEventsSearchResponse = operations['notes___events___search']['responses']['200']['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 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']; -export type NotesRepliesResponse = operations['notes___replies']['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 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']; -export type NotesStateResponse = operations['notes___state']['responses']['200']['content']['application/json']; -export type NotesThreadMutingCreateRequest = operations['notes___thread-muting___create']['requestBody']['content']['application/json']; -export type NotesThreadMutingDeleteRequest = operations['notes___thread-muting___delete']['requestBody']['content']['application/json']; -export type NotesTimelineRequest = operations['notes___timeline']['requestBody']['content']['application/json']; -export type NotesTimelineResponse = operations['notes___timeline']['responses']['200']['content']['application/json']; -export type NotesTranslateRequest = operations['notes___translate']['requestBody']['content']['application/json']; -export type NotesTranslateResponse = operations['notes___translate']['responses']['200']['content']['application/json']; -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 NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json']; -export type NotificationsDeleteRequest = operations['notifications___delete']['requestBody']['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']; +export type NotesClipsResponse = operations['notes/clips']['responses']['200']['content']['application/json']; +export type NotesConversationRequest = operations['notes/conversation']['requestBody']['content']['application/json']; +export type NotesConversationResponse = operations['notes/conversation']['responses']['200']['content']['application/json']; +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 NotesUpdateRequest = operations['notes/update']['requestBody']['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']; +export type NotesFeaturedResponse = operations['notes/featured']['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 NotesHybridTimelineRequest = operations['notes/hybrid-timeline']['requestBody']['content']['application/json']; +export type NotesHybridTimelineResponse = operations['notes/hybrid-timeline']['responses']['200']['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 NotesEventsSearchRequest = operations['notes/events/search']['requestBody']['content']['application/json']; +export type NotesEventsSearchResponse = operations['notes/events/search']['responses']['200']['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 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']; +export type NotesRepliesResponse = operations['notes/replies']['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 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']; +export type NotesStateResponse = operations['notes/state']['responses']['200']['content']['application/json']; +export type NotesThreadMutingCreateRequest = operations['notes/thread-muting/create']['requestBody']['content']['application/json']; +export type NotesThreadMutingDeleteRequest = operations['notes/thread-muting/delete']['requestBody']['content']['application/json']; +export type NotesTimelineRequest = operations['notes/timeline']['requestBody']['content']['application/json']; +export type NotesTimelineResponse = operations['notes/timeline']['responses']['200']['content']['application/json']; +export type NotesTranslateRequest = operations['notes/translate']['requestBody']['content']['application/json']; +export type NotesTranslateResponse = operations['notes/translate']['responses']['200']['content']['application/json']; +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 NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json']; export type PagePushRequest = operations['page-push']['requestBody']['content']['application/json']; -export type PagesCreateRequest = operations['pages___create']['requestBody']['content']['application/json']; -export type PagesCreateResponse = operations['pages___create']['responses']['200']['content']['application/json']; -export type PagesDeleteRequest = operations['pages___delete']['requestBody']['content']['application/json']; -export type PagesFeaturedResponse = operations['pages___featured']['responses']['200']['content']['application/json']; -export type PagesLikeRequest = operations['pages___like']['requestBody']['content']['application/json']; -export type PagesShowRequest = operations['pages___show']['requestBody']['content']['application/json']; -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 FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json']; -export type FlashGenTokenRequest = operations['flash___gen-token']['requestBody']['content']['application/json']; -export type FlashGenTokenResponse = operations['flash___gen-token']['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 PagesCreateRequest = operations['pages/create']['requestBody']['content']['application/json']; +export type PagesCreateResponse = operations['pages/create']['responses']['200']['content']['application/json']; +export type PagesDeleteRequest = operations['pages/delete']['requestBody']['content']['application/json']; +export type PagesFeaturedResponse = operations['pages/featured']['responses']['200']['content']['application/json']; +export type PagesLikeRequest = operations['pages/like']['requestBody']['content']['application/json']; +export type PagesShowRequest = operations['pages/show']['requestBody']['content']['application/json']; +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 FlashFeaturedResponse = operations['flash/featured']['responses']['200']['content']['application/json']; +export type FlashGenTokenRequest = operations['flash/gen-token']['requestBody']['content']['application/json']; +export type FlashGenTokenResponse = operations['flash/gen-token']['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 RolesListResponse = operations['roles___list']['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 PromoReadRequest = operations['promo/read']['requestBody']['content']['application/json']; +export type RolesListResponse = operations['roles/list']['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 StatsResponse = operations['stats']['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 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 SwShowRegistrationRequest = operations['sw/show-registration']['requestBody']['content']['application/json']; +export type SwShowRegistrationResponse = operations['sw/show-registration']['responses']['200']['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 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 UsersClipsRequest = operations['users___clips']['requestBody']['content']['application/json']; -export type UsersClipsResponse = operations['users___clips']['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']; -export type UsersFollowingResponse = operations['users___following']['responses']['200']['content']['application/json']; -export type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBody']['content']['application/json']; -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 UsersGroupsCreateRequest = operations['users___groups___create']['requestBody']['content']['application/json']; -export type UsersGroupsCreateResponse = operations['users___groups___create']['responses']['200']['content']['application/json']; -export type UsersGroupsDeleteRequest = operations['users___groups___delete']['requestBody']['content']['application/json']; -export type UsersGroupsInvitationsAcceptRequest = operations['users___groups___invitations___accept']['requestBody']['content']['application/json']; -export type UsersGroupsInvitationsRejectRequest = operations['users___groups___invitations___reject']['requestBody']['content']['application/json']; -export type UsersGroupsInviteRequest = operations['users___groups___invite']['requestBody']['content']['application/json']; -export type UsersGroupsJoinedResponse = operations['users___groups___joined']['responses']['200']['content']['application/json']; -export type UsersGroupsLeaveRequest = operations['users___groups___leave']['requestBody']['content']['application/json']; -export type UsersGroupsOwnedResponse = operations['users___groups___owned']['responses']['200']['content']['application/json']; -export type UsersGroupsPullRequest = operations['users___groups___pull']['requestBody']['content']['application/json']; -export type UsersGroupsShowRequest = operations['users___groups___show']['requestBody']['content']['application/json']; -export type UsersGroupsShowResponse = operations['users___groups___show']['responses']['200']['content']['application/json']; -export type UsersGroupsTransferRequest = operations['users___groups___transfer']['requestBody']['content']['application/json']; -export type UsersGroupsTransferResponse = operations['users___groups___transfer']['responses']['200']['content']['application/json']; -export type UsersGroupsUpdateRequest = operations['users___groups___update']['requestBody']['content']['application/json']; -export type UsersGroupsUpdateResponse = operations['users___groups___update']['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 UsersListsDeleteRequest = operations['users___lists___delete']['requestBody']['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']; -export type UsersRecommendationResponse = operations['users___recommendation']['responses']['200']['content']['application/json']; -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 UsersShowRequest = operations['users___show']['requestBody']['content']['application/json']; -export type UsersShowResponse = operations['users___show']['responses']['200']['content']['application/json']; -export type UsersStatsRequest = operations['users___stats']['requestBody']['content']['application/json']; -export type UsersStatsResponse = operations['users___stats']['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 UsersTranslateRequest = operations['users___translate']['requestBody']['content']['application/json']; -export type UsersTranslateResponse = operations['users___translate']['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 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']; +export type UsersFollowingResponse = operations['users/following']['responses']['200']['content']['application/json']; +export type UsersGalleryPostsRequest = operations['users/gallery/posts']['requestBody']['content']['application/json']; +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 UsersGroupsCreateRequest = operations['users/groups/create']['requestBody']['content']['application/json']; +export type UsersGroupsCreateResponse = operations['users/groups/create']['responses']['200']['content']['application/json']; +export type UsersGroupsDeleteRequest = operations['users/groups/delete']['requestBody']['content']['application/json']; +export type UsersGroupsInvitationsAcceptRequest = operations['users/groups/invitations/accept']['requestBody']['content']['application/json']; +export type UsersGroupsInvitationsRejectRequest = operations['users/groups/invitations/reject']['requestBody']['content']['application/json']; +export type UsersGroupsInviteRequest = operations['users/groups/invite']['requestBody']['content']['application/json']; +export type UsersGroupsJoinedResponse = operations['users/groups/joined']['responses']['200']['content']['application/json']; +export type UsersGroupsLeaveRequest = operations['users/groups/leave']['requestBody']['content']['application/json']; +export type UsersGroupsOwnedResponse = operations['users/groups/owned']['responses']['200']['content']['application/json']; +export type UsersGroupsPullRequest = operations['users/groups/pull']['requestBody']['content']['application/json']; +export type UsersGroupsShowRequest = operations['users/groups/show']['requestBody']['content']['application/json']; +export type UsersGroupsShowResponse = operations['users/groups/show']['responses']['200']['content']['application/json']; +export type UsersGroupsTransferRequest = operations['users/groups/transfer']['requestBody']['content']['application/json']; +export type UsersGroupsTransferResponse = operations['users/groups/transfer']['responses']['200']['content']['application/json']; +export type UsersGroupsUpdateRequest = operations['users/groups/update']['requestBody']['content']['application/json']; +export type UsersGroupsUpdateResponse = operations['users/groups/update']['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 UsersListsDeleteRequest = operations['users/lists/delete']['requestBody']['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']; +export type UsersRecommendationResponse = operations['users/recommendation']['responses']['200']['content']['application/json']; +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 UsersShowRequest = operations['users/show']['requestBody']['content']['application/json']; +export type UsersShowResponse = operations['users/show']['responses']['200']['content']['application/json']; +export type UsersStatsRequest = operations['users/stats']['requestBody']['content']['application/json']; +export type UsersStatsResponse = operations['users/stats']['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 UsersTranslateRequest = operations['users/translate']['requestBody']['content']['application/json']; +export type UsersTranslateResponse = operations['users/translate']['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 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 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']; diff --git a/packages/cherrypick-js/src/autogen/models.ts b/packages/cherrypick-js/src/autogen/models.ts index 5922248f15..ceedaf436a 100644 --- a/packages/cherrypick-js/src/autogen/models.ts +++ b/packages/cherrypick-js/src/autogen/models.ts @@ -1,3 +1,9 @@ +/* + * version: 4.6.0 + * basedMisskeyVersion: 2023.12.2 + * generatedAt: 2024-01-08T10:34:58.480Z + */ + import { components } from './types.js'; export type Error = components['schemas']['Error']; export type UserLite = components['schemas']['UserLite']; @@ -26,7 +32,6 @@ export type Blocking = components['schemas']['Blocking']; export type Hashtag = components['schemas']['Hashtag']; export type InviteCode = components['schemas']['InviteCode']; export type Page = components['schemas']['Page']; -export type PageBlock = components['schemas']['PageBlock']; export type Channel = components['schemas']['Channel']; export type QueueCount = components['schemas']['QueueCount']; export type Antenna = components['schemas']['Antenna']; @@ -37,19 +42,5 @@ export type EmojiSimple = components['schemas']['EmojiSimple']; export type EmojiDetailed = components['schemas']['EmojiDetailed']; export type Flash = components['schemas']['Flash']; export type Signin = components['schemas']['Signin']; -export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics']; -export type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot']; -export type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote']; -export type RoleCondFormulaValueUserSettingBooleanSchema = components['schemas']['RoleCondFormulaValueUserSettingBooleanSchema']; -export type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole']; -export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated']; -export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; -export type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue']; export type RoleLite = components['schemas']['RoleLite']; export type Role = components['schemas']['Role']; -export type RolePolicies = components['schemas']['RolePolicies']; -export type MetaLite = components['schemas']['MetaLite']; -export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly']; -export type MetaDetailed = components['schemas']['MetaDetailed']; -export type SystemWebhook = components['schemas']['SystemWebhook']; -export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient']; diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index 0c8b4e68d7..4ef48d7b07 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -1,6 +1,12 @@ /* eslint @typescript-eslint/naming-convention: 0 */ /* eslint @typescript-eslint/no-explicit-any: 0 */ +/* + * version: 4.6.0 + * basedMisskeyVersion: 2023.12.2 + * generatedAt: 2024-01-08T10:34:58.404Z + */ + /** * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. @@ -19,47 +25,43 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:meta* */ - post: operations['admin___meta']; + post: operations['admin/meta']; }; '/admin/abuse-report-resolver/create': { /** * admin/abuse-report-resolver/create * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-create* + * **Credential required**: *Yes* */ - post: operations['admin___abuse-report-resolver___create']; + post: operations['admin/abuse-report-resolver/create']; }; '/admin/abuse-report-resolver/list': { /** * admin/abuse-report-resolver/list * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-list* + * **Credential required**: *Yes* */ - post: operations['admin___abuse-report-resolver___list']; + post: operations['admin/abuse-report-resolver/list']; }; '/admin/abuse-report-resolver/delete': { /** * admin/abuse-report-resolver/delete * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *No* / **Permission**: *arr-delete* + * **Credential required**: *No* */ - post: operations['admin___abuse-report-resolver___delete']; + post: operations['admin/abuse-report-resolver/delete']; }; '/admin/abuse-report-resolver/update': { /** * admin/abuse-report-resolver/update * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-update* + * **Credential required**: *Yes* */ - post: operations['admin___abuse-report-resolver___update']; + post: operations['admin/abuse-report-resolver/update']; }; '/admin/abuse-user-reports': { /** @@ -68,57 +70,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports* */ - post: operations['admin___abuse-user-reports']; - }; - '/admin/abuse-report/notification-recipient/list': { - /** - * admin/abuse-report/notification-recipient/list - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient* - */ - post: operations['admin___abuse-report___notification-recipient___list']; - }; - '/admin/abuse-report/notification-recipient/show': { - /** - * admin/abuse-report/notification-recipient/show - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient* - */ - 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 cherrypick 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 - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick 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___update']; - }; - '/admin/abuse-report/notification-recipient/delete': { - /** - * admin/abuse-report/notification-recipient/delete - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick 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___delete']; + post: operations['admin/abuse-user-reports']; }; '/admin/accounts/create': { /** @@ -127,7 +79,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['admin___accounts___create']; + post: operations['admin/accounts/create']; }; '/admin/accounts/delete': { /** @@ -136,7 +88,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:account* */ - post: operations['admin___accounts___delete']; + post: operations['admin/accounts/delete']; }; '/admin/accounts/find-by-email': { /** @@ -145,7 +97,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:account* */ - post: operations['admin___accounts___find-by-email']; + post: operations['admin/accounts/find-by-email']; }; '/admin/ad/create': { /** @@ -154,7 +106,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - post: operations['admin___ad___create']; + post: operations['admin/ad/create']; }; '/admin/ad/delete': { /** @@ -163,7 +115,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - post: operations['admin___ad___delete']; + post: operations['admin/ad/delete']; }; '/admin/ad/list': { /** @@ -172,7 +124,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:ad* */ - post: operations['admin___ad___list']; + post: operations['admin/ad/list']; }; '/admin/ad/update': { /** @@ -181,7 +133,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - post: operations['admin___ad___update']; + post: operations['admin/ad/update']; }; '/admin/announcements/create': { /** @@ -190,7 +142,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - post: operations['admin___announcements___create']; + post: operations['admin/announcements/create']; }; '/admin/announcements/delete': { /** @@ -199,7 +151,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - post: operations['admin___announcements___delete']; + post: operations['admin/announcements/delete']; }; '/admin/announcements/list': { /** @@ -208,7 +160,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:announcements* */ - post: operations['admin___announcements___list']; + post: operations['admin/announcements/list']; }; '/admin/announcements/update': { /** @@ -217,7 +169,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - post: operations['admin___announcements___update']; + post: operations['admin/announcements/update']; }; '/admin/avatar-decorations/create': { /** @@ -226,7 +178,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - post: operations['admin___avatar-decorations___create']; + post: operations['admin/avatar-decorations/create']; }; '/admin/avatar-decorations/delete': { /** @@ -235,7 +187,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - post: operations['admin___avatar-decorations___delete']; + post: operations['admin/avatar-decorations/delete']; }; '/admin/avatar-decorations/list': { /** @@ -244,7 +196,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:avatar-decorations* */ - post: operations['admin___avatar-decorations___list']; + post: operations['admin/avatar-decorations/list']; }; '/admin/avatar-decorations/update': { /** @@ -253,7 +205,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - post: operations['admin___avatar-decorations___update']; + post: operations['admin/avatar-decorations/update']; }; '/admin/delete-all-files-of-a-user': { /** @@ -262,7 +214,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user* */ - post: operations['admin___delete-all-files-of-a-user']; + post: operations['admin/delete-all-files-of-a-user']; }; '/admin/unset-user-avatar': { /** @@ -271,7 +223,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar* */ - post: operations['admin___unset-user-avatar']; + post: operations['admin/unset-user-avatar']; }; '/admin/unset-user-banner': { /** @@ -280,7 +232,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner* */ - post: operations['admin___unset-user-banner']; + post: operations['admin/unset-user-banner']; }; '/admin/drive/clean-remote-files': { /** @@ -289,7 +241,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:drive* */ - post: operations['admin___drive___clean-remote-files']; + post: operations['admin/drive/clean-remote-files']; }; '/admin/drive/cleanup': { /** @@ -298,7 +250,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:drive* */ - post: operations['admin___drive___cleanup']; + post: operations['admin/drive/cleanup']; }; '/admin/drive/files': { /** @@ -307,7 +259,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:drive* */ - post: operations['admin___drive___files']; + post: operations['admin/drive/files']; }; '/admin/drive/show-file': { /** @@ -316,7 +268,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:drive* */ - post: operations['admin___drive___show-file']; + post: operations['admin/drive/show-file']; }; '/admin/emoji/add-aliases-bulk': { /** @@ -325,7 +277,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___add-aliases-bulk']; + post: operations['admin/emoji/add-aliases-bulk']; }; '/admin/emoji/add': { /** @@ -334,7 +286,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___add']; + post: operations['admin/emoji/add']; }; '/admin/emoji/adds': { /** @@ -343,7 +295,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___adds']; + post: operations['admin/emoji/adds']; }; '/admin/emoji/copy': { /** @@ -352,7 +304,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___copy']; + post: operations['admin/emoji/copy']; }; '/admin/emoji/delete-bulk': { /** @@ -361,7 +313,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___delete-bulk']; + post: operations['admin/emoji/delete-bulk']; }; '/admin/emoji/delete': { /** @@ -370,7 +322,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___delete']; + post: operations['admin/emoji/delete']; }; '/admin/emoji/import-zip': { /** @@ -380,7 +332,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['admin___emoji___import-zip']; + post: operations['admin/emoji/import-zip']; }; '/admin/emoji/list-remote': { /** @@ -389,7 +341,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ - post: operations['admin___emoji___list-remote']; + post: operations['admin/emoji/list-remote']; }; '/admin/emoji/list': { /** @@ -398,7 +350,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ - post: operations['admin___emoji___list']; + post: operations['admin/emoji/list']; }; '/admin/emoji/remove-aliases-bulk': { /** @@ -407,7 +359,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___remove-aliases-bulk']; + post: operations['admin/emoji/remove-aliases-bulk']; }; '/admin/emoji/set-aliases-bulk': { /** @@ -416,7 +368,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___set-aliases-bulk']; + post: operations['admin/emoji/set-aliases-bulk']; }; '/admin/emoji/set-category-bulk': { /** @@ -425,7 +377,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___set-category-bulk']; + post: operations['admin/emoji/set-category-bulk']; }; '/admin/emoji/set-license-bulk': { /** @@ -434,7 +386,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___set-license-bulk']; + post: operations['admin/emoji/set-license-bulk']; }; '/admin/emoji/steal': { /** @@ -443,7 +395,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___steal']; + post: operations['admin/emoji/steal']; }; '/admin/emoji/update': { /** @@ -452,7 +404,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - post: operations['admin___emoji___update']; + post: operations['admin/emoji/update']; }; '/admin/federation/delete-all-files': { /** @@ -461,7 +413,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ - post: operations['admin___federation___delete-all-files']; + post: operations['admin/federation/delete-all-files']; }; '/admin/federation/refresh-remote-instance-metadata': { /** @@ -470,7 +422,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ - post: operations['admin___federation___refresh-remote-instance-metadata']; + post: operations['admin/federation/refresh-remote-instance-metadata']; }; '/admin/federation/remove-all-following': { /** @@ -479,7 +431,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ - post: operations['admin___federation___remove-all-following']; + post: operations['admin/federation/remove-all-following']; }; '/admin/federation/update-instance': { /** @@ -488,7 +440,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ - post: operations['admin___federation___update-instance']; + post: operations['admin/federation/update-instance']; }; '/admin/get-index-stats': { /** @@ -497,7 +449,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:index-stats* */ - post: operations['admin___get-index-stats']; + post: operations['admin/get-index-stats']; }; '/admin/get-table-stats': { /** @@ -506,7 +458,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:table-stats* */ - post: operations['admin___get-table-stats']; + post: operations['admin/get-table-stats']; }; '/admin/get-user-ips': { /** @@ -515,7 +467,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:user-ips* */ - post: operations['admin___get-user-ips']; + post: operations['admin/get-user-ips']; }; '/admin/invite/create': { /** @@ -524,7 +476,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:invite-codes* */ - post: operations['admin___invite___create']; + post: operations['admin/invite/create']; }; '/admin/invite/list': { /** @@ -533,7 +485,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:invite-codes* */ - post: operations['admin___invite___list']; + post: operations['admin/invite/list']; }; '/admin/invite/revoke': { /** @@ -542,7 +494,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:invite-codes* */ - post: operations['admin___invite___revoke']; + post: operations['admin/invite/revoke']; }; '/admin/promo/create': { /** @@ -551,7 +503,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:promo* */ - post: operations['admin___promo___create']; + post: operations['admin/promo/create']; }; '/admin/queue/clear': { /** @@ -560,7 +512,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ - post: operations['admin___queue___clear']; + post: operations['admin/queue/clear']; }; '/admin/queue/deliver-delayed': { /** @@ -569,7 +521,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:queue* */ - post: operations['admin___queue___deliver-delayed']; + post: operations['admin/queue/deliver-delayed']; }; '/admin/queue/inbox-delayed': { /** @@ -578,7 +530,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:queue* */ - post: operations['admin___queue___inbox-delayed']; + post: operations['admin/queue/inbox-delayed']; }; '/admin/queue/promote': { /** @@ -587,7 +539,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ - post: operations['admin___queue___promote']; + post: operations['admin/queue/promote']; }; '/admin/queue/stats': { /** @@ -596,7 +548,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ - post: operations['admin___queue___stats']; + post: operations['admin/queue/stats']; }; '/admin/relays/add': { /** @@ -605,7 +557,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:relays* */ - post: operations['admin___relays___add']; + post: operations['admin/relays/add']; }; '/admin/relays/list': { /** @@ -614,7 +566,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:relays* */ - post: operations['admin___relays___list']; + post: operations['admin/relays/list']; }; '/admin/relays/remove': { /** @@ -623,7 +575,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:relays* */ - post: operations['admin___relays___remove']; + post: operations['admin/relays/remove']; }; '/admin/reset-password': { /** @@ -632,7 +584,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:reset-password* */ - post: operations['admin___reset-password']; + post: operations['admin/reset-password']; }; '/admin/resolve-abuse-user-report': { /** @@ -641,7 +593,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report* */ - post: operations['admin___resolve-abuse-user-report']; + post: operations['admin/resolve-abuse-user-report']; }; '/admin/send-email': { /** @@ -650,7 +602,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:send-email* */ - post: operations['admin___send-email']; + post: operations['admin/send-email']; }; '/admin/server-info': { /** @@ -659,7 +611,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:server-info* */ - post: operations['admin___server-info']; + post: operations['admin/server-info']; }; '/admin/show-moderation-logs': { /** @@ -668,7 +620,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log* */ - post: operations['admin___show-moderation-logs']; + post: operations['admin/show-moderation-logs']; }; '/admin/show-user': { /** @@ -677,16 +629,16 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:show-user* */ - post: operations['admin___show-user']; + post: operations['admin/show-user']; }; '/admin/show-users': { /** * admin/show-users * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:show-user* + * **Credential required**: *Yes* / **Permission**: *read:admin:show-users* */ - post: operations['admin___show-users']; + post: operations['admin/show-users']; }; '/admin/suspend-user': { /** @@ -695,7 +647,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* */ - post: operations['admin___suspend-user']; + post: operations['admin/suspend-user']; }; '/admin/unsuspend-user': { /** @@ -704,25 +656,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user* */ - post: operations['admin___unsuspend-user']; - }; - '/admin/set-user-sensitive': { - /** - * admin/set-user-sensitive - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* - */ - post: operations['admin___set-user-sensitive']; - }; - '/admin/unset-user-sensitive': { - /** - * admin/unset-user-sensitive - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* - */ - post: operations['admin___unset-user-sensitive']; + post: operations['admin/unsuspend-user']; }; '/admin/update-meta': { /** @@ -731,7 +665,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:meta* */ - post: operations['admin___update-meta']; + post: operations['admin/update-meta']; }; '/admin/delete-account': { /** @@ -740,7 +674,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account* */ - post: operations['admin___delete-account']; + post: operations['admin/delete-account']; }; '/admin/update-user-note': { /** @@ -749,7 +683,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:user-note* */ - post: operations['admin___update-user-note']; + post: operations['admin/update-user-note']; }; '/admin/roles/create': { /** @@ -758,7 +692,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - post: operations['admin___roles___create']; + post: operations['admin/roles/create']; }; '/admin/roles/delete': { /** @@ -767,7 +701,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - post: operations['admin___roles___delete']; + post: operations['admin/roles/delete']; }; '/admin/roles/list': { /** @@ -776,7 +710,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:roles* */ - post: operations['admin___roles___list']; + post: operations['admin/roles/list']; }; '/admin/roles/show': { /** @@ -785,7 +719,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:admin:roles* */ - post: operations['admin___roles___show']; + post: operations['admin/roles/show']; }; '/admin/roles/update': { /** @@ -794,7 +728,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - post: operations['admin___roles___update']; + post: operations['admin/roles/update']; }; '/admin/roles/assign': { /** @@ -803,7 +737,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - post: operations['admin___roles___assign']; + post: operations['admin/roles/assign']; }; '/admin/roles/unassign': { /** @@ -812,7 +746,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - post: operations['admin___roles___unassign']; + post: operations['admin/roles/unassign']; }; '/admin/roles/update-default-policies': { /** @@ -821,7 +755,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - post: operations['admin___roles___update-default-policies']; + post: operations['admin/roles/update-default-policies']; }; '/admin/roles/users': { /** @@ -830,57 +764,7 @@ export type paths = { * * **Credential required**: *No* / **Permission**: *read:admin:roles* */ - post: operations['admin___roles___users']; - }; - '/admin/system-webhook/create': { - /** - * admin/system-webhook/create - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - post: operations['admin___system-webhook___create']; - }; - '/admin/system-webhook/delete': { - /** - * admin/system-webhook/delete - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - post: operations['admin___system-webhook___delete']; - }; - '/admin/system-webhook/list': { - /** - * admin/system-webhook/list - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - post: operations['admin___system-webhook___list']; - }; - '/admin/system-webhook/show': { - /** - * admin/system-webhook/show - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - post: operations['admin___system-webhook___show']; - }; - '/admin/system-webhook/update': { - /** - * admin/system-webhook/update - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* - */ - post: operations['admin___system-webhook___update']; + post: operations['admin/roles/users']; }; '/announcements': { /** @@ -891,15 +775,6 @@ export type paths = { */ post: operations['announcements']; }; - '/announcements/show': { - /** - * announcements/show - * @description No description provided. - * - * **Credential required**: *No* - */ - post: operations['announcements___show']; - }; '/antennas/create': { /** * antennas/create @@ -907,7 +782,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['antennas___create']; + post: operations['antennas/create']; }; '/antennas/delete': { /** @@ -916,7 +791,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['antennas___delete']; + post: operations['antennas/delete']; }; '/antennas/list': { /** @@ -925,7 +800,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['antennas___list']; + post: operations['antennas/list']; }; '/antennas/notes': { /** @@ -934,7 +809,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['antennas___notes']; + post: operations['antennas/notes']; }; '/antennas/show': { /** @@ -943,7 +818,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['antennas___show']; + post: operations['antennas/show']; }; '/antennas/update': { /** @@ -952,7 +827,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['antennas___update']; + post: operations['antennas/update']; }; '/ap/get': { /** @@ -961,7 +836,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:federation* */ - post: operations['ap___get']; + post: operations['ap/get']; }; '/ap/show': { /** @@ -970,7 +845,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['ap___show']; + post: operations['ap/show']; }; '/app/create': { /** @@ -979,7 +854,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['app___create']; + post: operations['app/create']; }; '/app/show': { /** @@ -988,7 +863,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['app___show']; + post: operations['app/show']; }; '/auth/accept': { /** @@ -998,7 +873,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['auth___accept']; + post: operations['auth/accept']; }; '/auth/session/generate': { /** @@ -1007,7 +882,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['auth___session___generate']; + post: operations['auth/session/generate']; }; '/auth/session/show': { /** @@ -1016,7 +891,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['auth___session___show']; + post: operations['auth/session/show']; }; '/auth/session/userkey': { /** @@ -1025,7 +900,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['auth___session___userkey']; + post: operations['auth/session/userkey']; }; '/blocking/create': { /** @@ -1034,7 +909,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:blocks* */ - post: operations['blocking___create']; + post: operations['blocking/create']; }; '/blocking/delete': { /** @@ -1043,7 +918,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:blocks* */ - post: operations['blocking___delete']; + post: operations['blocking/delete']; }; '/blocking/list': { /** @@ -1052,7 +927,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:blocks* */ - post: operations['blocking___list']; + post: operations['blocking/list']; }; '/channels/create': { /** @@ -1061,7 +936,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - post: operations['channels___create']; + post: operations['channels/create']; }; '/channels/featured': { /** @@ -1070,7 +945,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['channels___featured']; + post: operations['channels/featured']; }; '/channels/follow': { /** @@ -1079,7 +954,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - post: operations['channels___follow']; + post: operations['channels/follow']; }; '/channels/followed': { /** @@ -1088,7 +963,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:channels* */ - post: operations['channels___followed']; + post: operations['channels/followed']; }; '/channels/owned': { /** @@ -1097,7 +972,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:channels* */ - post: operations['channels___owned']; + post: operations['channels/owned']; }; '/channels/show': { /** @@ -1106,7 +981,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['channels___show']; + post: operations['channels/show']; }; '/channels/timeline': { /** @@ -1115,7 +990,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['channels___timeline']; + post: operations['channels/timeline']; }; '/channels/unfollow': { /** @@ -1124,7 +999,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - post: operations['channels___unfollow']; + post: operations['channels/unfollow']; }; '/channels/update': { /** @@ -1133,7 +1008,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - post: operations['channels___update']; + post: operations['channels/update']; }; '/channels/favorite': { /** @@ -1142,7 +1017,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - post: operations['channels___favorite']; + post: operations['channels/favorite']; }; '/channels/unfavorite': { /** @@ -1151,7 +1026,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - post: operations['channels___unfavorite']; + post: operations['channels/unfavorite']; }; '/channels/my-favorites': { /** @@ -1160,7 +1035,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:channels* */ - post: operations['channels___my-favorites']; + post: operations['channels/my-favorites']; }; '/channels/search': { /** @@ -1169,7 +1044,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['channels___search']; + post: operations['channels/search']; }; '/charts/active-users': { /** @@ -1178,14 +1053,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___active-users']; + get: operations['charts/active-users']; /** * charts/active-users * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___active-users']; + post: operations['charts/active-users']; }; '/charts/ap-request': { /** @@ -1194,14 +1069,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___ap-request']; + get: operations['charts/ap-request']; /** * charts/ap-request * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___ap-request']; + post: operations['charts/ap-request']; }; '/charts/drive': { /** @@ -1210,14 +1085,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___drive']; + get: operations['charts/drive']; /** * charts/drive * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___drive']; + post: operations['charts/drive']; }; '/charts/federation': { /** @@ -1226,14 +1101,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___federation']; + get: operations['charts/federation']; /** * charts/federation * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___federation']; + post: operations['charts/federation']; }; '/charts/instance': { /** @@ -1242,14 +1117,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___instance']; + get: operations['charts/instance']; /** * charts/instance * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___instance']; + post: operations['charts/instance']; }; '/charts/notes': { /** @@ -1258,14 +1133,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___notes']; + get: operations['charts/notes']; /** * charts/notes * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___notes']; + post: operations['charts/notes']; }; '/charts/user/drive': { /** @@ -1274,14 +1149,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___user___drive']; + get: operations['charts/user/drive']; /** * charts/user/drive * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___user___drive']; + post: operations['charts/user/drive']; }; '/charts/user/following': { /** @@ -1290,14 +1165,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___user___following']; + get: operations['charts/user/following']; /** * charts/user/following * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___user___following']; + post: operations['charts/user/following']; }; '/charts/user/notes': { /** @@ -1306,14 +1181,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___user___notes']; + get: operations['charts/user/notes']; /** * charts/user/notes * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___user___notes']; + post: operations['charts/user/notes']; }; '/charts/user/pv': { /** @@ -1322,14 +1197,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___user___pv']; + get: operations['charts/user/pv']; /** * charts/user/pv * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___user___pv']; + post: operations['charts/user/pv']; }; '/charts/user/reactions': { /** @@ -1338,14 +1213,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___user___reactions']; + get: operations['charts/user/reactions']; /** * charts/user/reactions * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___user___reactions']; + post: operations['charts/user/reactions']; }; '/charts/users': { /** @@ -1354,14 +1229,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['charts___users']; + get: operations['charts/users']; /** * charts/users * @description No description provided. * * **Credential required**: *No* */ - post: operations['charts___users']; + post: operations['charts/users']; }; '/clips/add-note': { /** @@ -1370,7 +1245,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['clips___add-note']; + post: operations['clips/add-note']; }; '/clips/remove-note': { /** @@ -1379,7 +1254,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['clips___remove-note']; + post: operations['clips/remove-note']; }; '/clips/create': { /** @@ -1388,7 +1263,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['clips___create']; + post: operations['clips/create']; }; '/clips/delete': { /** @@ -1397,7 +1272,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['clips___delete']; + post: operations['clips/delete']; }; '/clips/list': { /** @@ -1406,7 +1281,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['clips___list']; + post: operations['clips/list']; }; '/clips/notes': { /** @@ -1415,7 +1290,7 @@ export type paths = { * * **Credential required**: *No* / **Permission**: *read:account* */ - post: operations['clips___notes']; + post: operations['clips/notes']; }; '/clips/show': { /** @@ -1424,7 +1299,7 @@ export type paths = { * * **Credential required**: *No* / **Permission**: *read:account* */ - post: operations['clips___show']; + post: operations['clips/show']; }; '/clips/update': { /** @@ -1433,7 +1308,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['clips___update']; + post: operations['clips/update']; }; '/clips/favorite': { /** @@ -1442,7 +1317,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* */ - post: operations['clips___favorite']; + post: operations['clips/favorite']; }; '/clips/unfavorite': { /** @@ -1451,7 +1326,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* */ - post: operations['clips___unfavorite']; + post: operations['clips/unfavorite']; }; '/clips/my-favorites': { /** @@ -1460,7 +1335,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:clip-favorite* */ - post: operations['clips___my-favorites']; + post: operations['clips/my-favorites']; }; '/drive': { /** @@ -1478,7 +1353,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___files']; + post: operations['drive/files']; }; '/drive/files/attached-notes': { /** @@ -1487,7 +1362,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___files___attached-notes']; + post: operations['drive/files/attached-notes']; }; '/drive/files/check-existence': { /** @@ -1496,7 +1371,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___files___check-existence']; + post: operations['drive/files/check-existence']; }; '/drive/files/create': { /** @@ -1505,7 +1380,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - post: operations['drive___files___create']; + post: operations['drive/files/create']; }; '/drive/files/delete': { /** @@ -1514,7 +1389,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - post: operations['drive___files___delete']; + post: operations['drive/files/delete']; }; '/drive/files/find-by-hash': { /** @@ -1523,7 +1398,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___files___find-by-hash']; + post: operations['drive/files/find-by-hash']; }; '/drive/files/find': { /** @@ -1532,7 +1407,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___files___find']; + post: operations['drive/files/find']; }; '/drive/files/show': { /** @@ -1541,7 +1416,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___files___show']; + post: operations['drive/files/show']; }; '/drive/files/update': { /** @@ -1550,7 +1425,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - post: operations['drive___files___update']; + post: operations['drive/files/update']; }; '/drive/files/upload-from-url': { /** @@ -1559,7 +1434,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - post: operations['drive___files___upload-from-url']; + post: operations['drive/files/upload-from-url']; }; '/drive/folders': { /** @@ -1568,7 +1443,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___folders']; + post: operations['drive/folders']; }; '/drive/folders/create': { /** @@ -1577,7 +1452,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - post: operations['drive___folders___create']; + post: operations['drive/folders/create']; }; '/drive/folders/delete': { /** @@ -1586,7 +1461,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - post: operations['drive___folders___delete']; + post: operations['drive/folders/delete']; }; '/drive/folders/find': { /** @@ -1595,7 +1470,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___folders___find']; + post: operations['drive/folders/find']; }; '/drive/folders/show': { /** @@ -1604,7 +1479,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___folders___show']; + post: operations['drive/folders/show']; }; '/drive/folders/update': { /** @@ -1613,7 +1488,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - post: operations['drive___folders___update']; + post: operations['drive/folders/update']; }; '/drive/stream': { /** @@ -1622,7 +1497,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - post: operations['drive___stream']; + post: operations['drive/stream']; }; '/email-address/available': { /** @@ -1631,7 +1506,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['email-address___available']; + post: operations['email-address/available']; }; '/endpoint': { /** @@ -1668,7 +1543,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['federation___followers']; + post: operations['federation/followers']; }; '/federation/following': { /** @@ -1677,7 +1552,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['federation___following']; + post: operations['federation/following']; }; '/federation/instances': { /** @@ -1686,14 +1561,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['federation___instances']; + get: operations['federation/instances']; /** * federation/instances * @description No description provided. * * **Credential required**: *No* */ - post: operations['federation___instances']; + post: operations['federation/instances']; }; '/federation/show-instance': { /** @@ -1702,7 +1577,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['federation___show-instance']; + post: operations['federation/show-instance']; }; '/federation/update-remote-user': { /** @@ -1711,7 +1586,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['federation___update-remote-user']; + post: operations['federation/update-remote-user']; }; '/federation/users': { /** @@ -1720,7 +1595,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['federation___users']; + post: operations['federation/users']; }; '/federation/stats': { /** @@ -1729,14 +1604,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['federation___stats']; + get: operations['federation/stats']; /** * federation/stats * @description No description provided. * * **Credential required**: *No* */ - post: operations['federation___stats']; + post: operations['federation/stats']; }; '/following/create': { /** @@ -1745,7 +1620,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - post: operations['following___create']; + post: operations['following/create']; }; '/following/delete': { /** @@ -1754,7 +1629,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - post: operations['following___delete']; + post: operations['following/delete']; }; '/following/update': { /** @@ -1763,7 +1638,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - post: operations['following___update']; + post: operations['following/update']; }; '/following/update-all': { /** @@ -1772,7 +1647,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - post: operations['following___update-all']; + post: operations['following/update-all']; }; '/following/invalidate': { /** @@ -1781,7 +1656,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - post: operations['following___invalidate']; + post: operations['following/invalidate']; }; '/following/requests/accept': { /** @@ -1790,7 +1665,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - post: operations['following___requests___accept']; + post: operations['following/requests/accept']; }; '/following/requests/cancel': { /** @@ -1799,7 +1674,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - post: operations['following___requests___cancel']; + post: operations['following/requests/cancel']; }; '/following/requests/list': { /** @@ -1808,7 +1683,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:following* */ - post: operations['following___requests___list']; + post: operations['following/requests/list']; }; '/following/requests/reject': { /** @@ -1817,7 +1692,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - post: operations['following___requests___reject']; + post: operations['following/requests/reject']; }; '/gallery/featured': { /** @@ -1826,7 +1701,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['gallery___featured']; + post: operations['gallery/featured']; }; '/gallery/popular': { /** @@ -1835,7 +1710,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['gallery___popular']; + post: operations['gallery/popular']; }; '/gallery/posts': { /** @@ -1844,7 +1719,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['gallery___posts']; + post: operations['gallery/posts']; }; '/gallery/posts/create': { /** @@ -1853,7 +1728,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ - post: operations['gallery___posts___create']; + post: operations['gallery/posts/create']; }; '/gallery/posts/delete': { /** @@ -1862,7 +1737,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ - post: operations['gallery___posts___delete']; + post: operations['gallery/posts/delete']; }; '/gallery/posts/like': { /** @@ -1871,7 +1746,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* */ - post: operations['gallery___posts___like']; + post: operations['gallery/posts/like']; }; '/gallery/posts/show': { /** @@ -1880,7 +1755,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['gallery___posts___show']; + post: operations['gallery/posts/show']; }; '/gallery/posts/unlike': { /** @@ -1889,7 +1764,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* */ - post: operations['gallery___posts___unlike']; + post: operations['gallery/posts/unlike']; }; '/gallery/posts/update': { /** @@ -1898,7 +1773,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ - post: operations['gallery___posts___update']; + post: operations['gallery/posts/update']; }; '/get-online-users-count': { /** @@ -1932,7 +1807,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['hashtags___list']; + post: operations['hashtags/list']; }; '/hashtags/search': { /** @@ -1941,7 +1816,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['hashtags___search']; + post: operations['hashtags/search']; }; '/hashtags/show': { /** @@ -1950,7 +1825,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['hashtags___show']; + post: operations['hashtags/show']; }; '/hashtags/trend': { /** @@ -1959,14 +1834,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['hashtags___trend']; + get: operations['hashtags/trend']; /** * hashtags/trend * @description No description provided. * * **Credential required**: *No* */ - post: operations['hashtags___trend']; + post: operations['hashtags/trend']; }; '/hashtags/users': { /** @@ -1975,7 +1850,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['hashtags___users']; + post: operations['hashtags/users']; }; '/i': { /** @@ -1994,7 +1869,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___2fa___done']; + post: operations['i/2fa/done']; }; '/i/2fa/key-done': { /** @@ -2004,7 +1879,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___2fa___key-done']; + post: operations['i/2fa/key-done']; }; '/i/2fa/password-less': { /** @@ -2014,7 +1889,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___2fa___password-less']; + post: operations['i/2fa/password-less']; }; '/i/2fa/register-key': { /** @@ -2024,7 +1899,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___2fa___register-key']; + post: operations['i/2fa/register-key']; }; '/i/2fa/register': { /** @@ -2034,7 +1909,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___2fa___register']; + post: operations['i/2fa/register']; }; '/i/2fa/update-key': { /** @@ -2044,7 +1919,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___2fa___update-key']; + post: operations['i/2fa/update-key']; }; '/i/2fa/remove-key': { /** @@ -2054,7 +1929,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___2fa___remove-key']; + post: operations['i/2fa/remove-key']; }; '/i/2fa/unregister': { /** @@ -2064,7 +1939,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___2fa___unregister']; + post: operations['i/2fa/unregister']; }; '/i/apps': { /** @@ -2074,7 +1949,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___apps']; + post: operations['i/apps']; }; '/i/authorized-apps': { /** @@ -2084,7 +1959,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___authorized-apps']; + post: operations['i/authorized-apps']; }; '/i/claim-achievement': { /** @@ -2093,7 +1968,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___claim-achievement']; + post: operations['i/claim-achievement']; }; '/i/change-password': { /** @@ -2103,7 +1978,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___change-password']; + post: operations['i/change-password']; }; '/i/delete-account': { /** @@ -2113,7 +1988,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___delete-account']; + post: operations['i/delete-account']; }; '/i/export-blocking': { /** @@ -2123,7 +1998,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___export-blocking']; + post: operations['i/export-blocking']; }; '/i/export-following': { /** @@ -2133,7 +2008,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___export-following']; + post: operations['i/export-following']; }; '/i/export-mute': { /** @@ -2143,7 +2018,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___export-mute']; + post: operations['i/export-mute']; }; '/i/export-notes': { /** @@ -2153,17 +2028,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___export-notes']; - }; - '/i/export-clips': { - /** - * i/export-clips - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* - */ - post: operations['i___export-clips']; + post: operations['i/export-notes']; }; '/i/export-favorites': { /** @@ -2173,7 +2038,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___export-favorites']; + post: operations['i/export-favorites']; }; '/i/export-user-lists': { /** @@ -2183,7 +2048,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___export-user-lists']; + post: operations['i/export-user-lists']; }; '/i/export-antennas': { /** @@ -2193,7 +2058,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___export-antennas']; + post: operations['i/export-antennas']; }; '/i/favorites': { /** @@ -2202,7 +2067,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:favorites* */ - post: operations['i___favorites']; + post: operations['i/favorites']; }; '/i/gallery/likes': { /** @@ -2211,7 +2076,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:gallery-likes* */ - post: operations['i___gallery___likes']; + post: operations['i/gallery/likes']; }; '/i/gallery/posts': { /** @@ -2220,7 +2085,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:gallery* */ - post: operations['i___gallery___posts']; + post: operations['i/gallery/posts']; }; '/i/import-blocking': { /** @@ -2230,7 +2095,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___import-blocking']; + post: operations['i/import-blocking']; }; '/i/import-following': { /** @@ -2240,7 +2105,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___import-following']; + post: operations['i/import-following']; }; '/i/import-muting': { /** @@ -2250,7 +2115,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___import-muting']; + post: operations['i/import-muting']; }; '/i/import-user-lists': { /** @@ -2260,7 +2125,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___import-user-lists']; + post: operations['i/import-user-lists']; }; '/i/import-antennas': { /** @@ -2270,7 +2135,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___import-antennas']; + post: operations['i/import-antennas']; }; '/i/notifications': { /** @@ -2279,7 +2144,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:notifications* */ - post: operations['i___notifications']; + post: operations['i/notifications']; }; '/i/notifications-grouped': { /** @@ -2288,7 +2153,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:notifications* */ - post: operations['i___notifications-grouped']; + post: operations['i/notifications-grouped']; }; '/i/page-likes': { /** @@ -2297,7 +2162,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:page-likes* */ - post: operations['i___page-likes']; + post: operations['i/page-likes']; }; '/i/pages': { /** @@ -2306,7 +2171,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:pages* */ - post: operations['i___pages']; + post: operations['i/pages']; }; '/i/pin': { /** @@ -2315,7 +2180,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___pin']; + post: operations['i/pin']; }; '/i/read-all-messaging-messages': { /** @@ -2324,7 +2189,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___read-all-messaging-messages']; + post: operations['i/read-all-messaging-messages']; }; '/i/read-all-unread-notes': { /** @@ -2333,7 +2198,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___read-all-unread-notes']; + post: operations['i/read-all-unread-notes']; }; '/i/read-announcement': { /** @@ -2342,7 +2207,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___read-announcement']; + post: operations['i/read-announcement']; }; '/i/regenerate-token': { /** @@ -2352,7 +2217,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___regenerate-token']; + post: operations['i/regenerate-token']; }; '/i/registry/get-all': { /** @@ -2361,7 +2226,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['i___registry___get-all']; + post: operations['i/registry/get-all']; }; '/i/registry/get-detail': { /** @@ -2370,7 +2235,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['i___registry___get-detail']; + post: operations['i/registry/get-detail']; }; '/i/registry/get': { /** @@ -2379,7 +2244,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['i___registry___get']; + post: operations['i/registry/get']; }; '/i/registry/keys-with-type': { /** @@ -2388,7 +2253,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['i___registry___keys-with-type']; + post: operations['i/registry/keys-with-type']; }; '/i/registry/keys': { /** @@ -2397,7 +2262,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['i___registry___keys']; + post: operations['i/registry/keys']; }; '/i/registry/remove': { /** @@ -2406,7 +2271,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___registry___remove']; + post: operations['i/registry/remove']; }; '/i/registry/scopes-with-domain': { /** @@ -2416,7 +2281,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___registry___scopes-with-domain']; + post: operations['i/registry/scopes-with-domain']; }; '/i/registry/set': { /** @@ -2425,7 +2290,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___registry___set']; + post: operations['i/registry/set']; }; '/i/revoke-token': { /** @@ -2435,7 +2300,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___revoke-token']; + post: operations['i/revoke-token']; }; '/i/signin-history': { /** @@ -2445,7 +2310,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___signin-history']; + post: operations['i/signin-history']; }; '/i/unpin': { /** @@ -2454,7 +2319,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___unpin']; + post: operations['i/unpin']; }; '/i/update-email': { /** @@ -2464,7 +2329,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___update-email']; + post: operations['i/update-email']; }; '/i/update': { /** @@ -2473,7 +2338,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___update']; + post: operations['i/update']; }; '/i/user-group-invites': { /** @@ -2482,7 +2347,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:user-groups* */ - post: operations['i___user-group-invites']; + post: operations['i/user-group-invites']; }; '/i/move': { /** @@ -2492,7 +2357,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['i___move']; + post: operations['i/move']; }; '/i/webhooks/create': { /** @@ -2501,7 +2366,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___webhooks___create']; + post: operations['i/webhooks/create']; }; '/i/webhooks/list': { /** @@ -2510,7 +2375,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['i___webhooks___list']; + post: operations['i/webhooks/list']; }; '/i/webhooks/show': { /** @@ -2519,7 +2384,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['i___webhooks___show']; + post: operations['i/webhooks/show']; }; '/i/webhooks/update': { /** @@ -2528,7 +2393,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___webhooks___update']; + post: operations['i/webhooks/update']; }; '/i/webhooks/delete': { /** @@ -2537,7 +2402,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['i___webhooks___delete']; + post: operations['i/webhooks/delete']; }; '/invite/create': { /** @@ -2546,7 +2411,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:invite-codes* */ - post: operations['invite___create']; + post: operations['invite/create']; }; '/invite/delete': { /** @@ -2555,7 +2420,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:invite-codes* */ - post: operations['invite___delete']; + post: operations['invite/delete']; }; '/invite/list': { /** @@ -2564,7 +2429,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:invite-codes* */ - post: operations['invite___list']; + post: operations['invite/list']; }; '/invite/limit': { /** @@ -2573,7 +2438,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:invite-codes* */ - post: operations['invite___limit']; + post: operations['invite/limit']; }; '/messaging/history': { /** @@ -2582,7 +2447,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:messaging* */ - post: operations['messaging___history']; + post: operations['messaging/history']; }; '/messaging/messages': { /** @@ -2591,7 +2456,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:messaging* */ - post: operations['messaging___messages']; + post: operations['messaging/messages']; }; '/messaging/messages/create': { /** @@ -2600,7 +2465,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:messaging* */ - post: operations['messaging___messages___create']; + post: operations['messaging/messages/create']; }; '/messaging/messages/delete': { /** @@ -2609,7 +2474,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:messaging* */ - post: operations['messaging___messages___delete']; + post: operations['messaging/messages/delete']; }; '/messaging/messages/read': { /** @@ -2618,7 +2483,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:messaging* */ - post: operations['messaging___messages___read']; + post: operations['messaging/messages/read']; }; '/meta': { /** @@ -2669,7 +2534,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['miauth___gen-token']; + post: operations['miauth/gen-token']; }; '/mute/create': { /** @@ -2678,7 +2543,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ - post: operations['mute___create']; + post: operations['mute/create']; }; '/mute/delete': { /** @@ -2687,7 +2552,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ - post: operations['mute___delete']; + post: operations['mute/delete']; }; '/mute/list': { /** @@ -2696,7 +2561,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:mutes* */ - post: operations['mute___list']; + post: operations['mute/list']; }; '/renote-mute/create': { /** @@ -2705,7 +2570,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ - post: operations['renote-mute___create']; + post: operations['renote-mute/create']; }; '/renote-mute/delete': { /** @@ -2714,7 +2579,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ - post: operations['renote-mute___delete']; + post: operations['renote-mute/delete']; }; '/renote-mute/list': { /** @@ -2723,7 +2588,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:mutes* */ - post: operations['renote-mute___list']; + post: operations['renote-mute/list']; }; '/my/apps': { /** @@ -2732,7 +2597,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['my___apps']; + post: operations['my/apps']; }; '/notes': { /** @@ -2750,7 +2615,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___children']; + post: operations['notes/children']; }; '/notes/clips': { /** @@ -2759,7 +2624,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___clips']; + post: operations['notes/clips']; }; '/notes/conversation': { /** @@ -2768,7 +2633,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___conversation']; + post: operations['notes/conversation']; }; '/notes/create': { /** @@ -2777,7 +2642,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - post: operations['notes___create']; + post: operations['notes/create']; }; '/notes/delete': { /** @@ -2786,7 +2651,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - post: operations['notes___delete']; + post: operations['notes/delete']; }; '/notes/update': { /** @@ -2795,7 +2660,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - post: operations['notes___update']; + post: operations['notes/update']; }; '/notes/favorites/create': { /** @@ -2804,7 +2669,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:favorites* */ - post: operations['notes___favorites___create']; + post: operations['notes/favorites/create']; }; '/notes/favorites/delete': { /** @@ -2813,7 +2678,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:favorites* */ - post: operations['notes___favorites___delete']; + post: operations['notes/favorites/delete']; }; '/notes/featured': { /** @@ -2822,14 +2687,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['notes___featured']; + get: operations['notes/featured']; /** * notes/featured * @description No description provided. * * **Credential required**: *No* */ - post: operations['notes___featured']; + post: operations['notes/featured']; }; '/notes/global-timeline': { /** @@ -2838,7 +2703,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___global-timeline']; + post: operations['notes/global-timeline']; }; '/notes/hybrid-timeline': { /** @@ -2847,7 +2712,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['notes___hybrid-timeline']; + post: operations['notes/hybrid-timeline']; }; '/notes/local-timeline': { /** @@ -2856,7 +2721,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___local-timeline']; + post: operations['notes/local-timeline']; }; '/notes/mentions': { /** @@ -2865,7 +2730,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['notes___mentions']; + post: operations['notes/mentions']; }; '/notes/polls/recommendation': { /** @@ -2874,7 +2739,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['notes___polls___recommendation']; + post: operations['notes/polls/recommendation']; }; '/notes/polls/vote': { /** @@ -2883,7 +2748,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:votes* */ - post: operations['notes___polls___vote']; + post: operations['notes/polls/vote']; }; '/notes/events/search': { /** @@ -2892,7 +2757,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___events___search']; + post: operations['notes/events/search']; }; '/notes/reactions': { /** @@ -2901,14 +2766,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['notes___reactions']; + get: operations['notes/reactions']; /** * notes/reactions * @description No description provided. * * **Credential required**: *No* */ - post: operations['notes___reactions']; + post: operations['notes/reactions']; }; '/notes/reactions/create': { /** @@ -2917,7 +2782,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:reactions* */ - post: operations['notes___reactions___create']; + post: operations['notes/reactions/create']; }; '/notes/reactions/delete': { /** @@ -2926,7 +2791,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:reactions* */ - post: operations['notes___reactions___delete']; + post: operations['notes/reactions/delete']; }; '/notes/renotes': { /** @@ -2935,7 +2800,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___renotes']; + post: operations['notes/renotes']; }; '/notes/replies': { /** @@ -2944,7 +2809,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___replies']; + post: operations['notes/replies']; }; '/notes/search-by-tag': { /** @@ -2953,7 +2818,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___search-by-tag']; + post: operations['notes/search-by-tag']; }; '/notes/search': { /** @@ -2962,7 +2827,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___search']; + post: operations['notes/search']; }; '/notes/show': { /** @@ -2971,7 +2836,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['notes___show']; + post: operations['notes/show']; }; '/notes/state': { /** @@ -2980,7 +2845,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['notes___state']; + post: operations['notes/state']; }; '/notes/thread-muting/create': { /** @@ -2989,7 +2854,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['notes___thread-muting___create']; + post: operations['notes/thread-muting/create']; }; '/notes/thread-muting/delete': { /** @@ -2998,7 +2863,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['notes___thread-muting___delete']; + post: operations['notes/thread-muting/delete']; }; '/notes/timeline': { /** @@ -3007,7 +2872,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['notes___timeline']; + post: operations['notes/timeline']; }; '/notes/translate': { /** @@ -3016,7 +2881,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['notes___translate']; + post: operations['notes/translate']; }; '/notes/unrenote': { /** @@ -3025,7 +2890,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - post: operations['notes___unrenote']; + post: operations['notes/unrenote']; }; '/notes/user-list-timeline': { /** @@ -3034,7 +2899,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['notes___user-list-timeline']; + post: operations['notes/user-list-timeline']; }; '/notifications/create': { /** @@ -3043,25 +2908,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ - post: operations['notifications___create']; - }; - '/notifications/delete': { - /** - * notifications/delete - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:notifications* - */ - post: operations['notifications___delete']; - }; - '/notifications/flush': { - /** - * notifications/flush - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:notifications* - */ - post: operations['notifications___flush']; + post: operations['notifications/create']; }; '/notifications/mark-all-as-read': { /** @@ -3070,7 +2917,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ - post: operations['notifications___mark-all-as-read']; + post: operations['notifications/mark-all-as-read']; }; '/notifications/test-notification': { /** @@ -3079,7 +2926,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ - post: operations['notifications___test-notification']; + post: operations['notifications/test-notification']; }; '/page-push': { /** @@ -3098,7 +2945,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:pages* */ - post: operations['pages___create']; + post: operations['pages/create']; }; '/pages/delete': { /** @@ -3107,7 +2954,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:pages* */ - post: operations['pages___delete']; + post: operations['pages/delete']; }; '/pages/featured': { /** @@ -3116,7 +2963,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['pages___featured']; + post: operations['pages/featured']; }; '/pages/like': { /** @@ -3125,7 +2972,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:page-likes* */ - post: operations['pages___like']; + post: operations['pages/like']; }; '/pages/show': { /** @@ -3134,7 +2981,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['pages___show']; + post: operations['pages/show']; }; '/pages/unlike': { /** @@ -3143,7 +2990,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:page-likes* */ - post: operations['pages___unlike']; + post: operations['pages/unlike']; }; '/pages/update': { /** @@ -3152,7 +2999,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:pages* */ - post: operations['pages___update']; + post: operations['pages/update']; }; '/flash/create': { /** @@ -3161,7 +3008,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:flash* */ - post: operations['flash___create']; + post: operations['flash/create']; }; '/flash/delete': { /** @@ -3170,7 +3017,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:flash* */ - post: operations['flash___delete']; + post: operations['flash/delete']; }; '/flash/featured': { /** @@ -3179,7 +3026,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['flash___featured']; + post: operations['flash/featured']; }; '/flash/gen-token': { /** @@ -3189,7 +3036,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['flash___gen-token']; + post: operations['flash/gen-token']; }; '/flash/like': { /** @@ -3198,7 +3045,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:flash-likes* */ - post: operations['flash___like']; + post: operations['flash/like']; }; '/flash/show': { /** @@ -3207,7 +3054,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['flash___show']; + post: operations['flash/show']; }; '/flash/unlike': { /** @@ -3216,7 +3063,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:flash-likes* */ - post: operations['flash___unlike']; + post: operations['flash/unlike']; }; '/flash/update': { /** @@ -3225,7 +3072,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:flash* */ - post: operations['flash___update']; + post: operations['flash/update']; }; '/flash/my': { /** @@ -3234,7 +3081,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:flash* */ - post: operations['flash___my']; + post: operations['flash/my']; }; '/flash/my-likes': { /** @@ -3243,7 +3090,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:flash-likes* */ - post: operations['flash___my-likes']; + post: operations['flash/my-likes']; }; '/ping': { /** @@ -3270,7 +3117,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['promo___read']; + post: operations['promo/read']; }; '/roles/list': { /** @@ -3279,7 +3126,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['roles___list']; + post: operations['roles/list']; }; '/roles/show': { /** @@ -3288,7 +3135,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['roles___show']; + post: operations['roles/show']; }; '/roles/users': { /** @@ -3297,7 +3144,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['roles___users']; + post: operations['roles/users']; }; '/roles/notes': { /** @@ -3306,7 +3153,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['roles___notes']; + post: operations['roles/notes']; }; '/request-reset-password': { /** @@ -3368,7 +3215,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['sw___show-registration']; + post: operations['sw/show-registration']; }; '/sw/update-registration': { /** @@ -3378,7 +3225,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['sw___update-registration']; + post: operations['sw/update-registration']; }; '/sw/register': { /** @@ -3388,7 +3235,7 @@ export type paths = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - post: operations['sw___register']; + post: operations['sw/register']; }; '/sw/unregister': { /** @@ -3397,7 +3244,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['sw___unregister']; + post: operations['sw/unregister']; }; '/test': { /** @@ -3415,7 +3262,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['username___available']; + post: operations['username/available']; }; '/users': { /** @@ -3433,7 +3280,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___clips']; + post: operations['users/clips']; }; '/users/followers': { /** @@ -3442,7 +3289,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___followers']; + post: operations['users/followers']; }; '/users/following': { /** @@ -3451,7 +3298,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___following']; + post: operations['users/following']; }; '/users/gallery/posts': { /** @@ -3460,7 +3307,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___gallery___posts']; + post: operations['users/gallery/posts']; }; '/users/get-frequently-replied-users': { /** @@ -3469,7 +3316,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___get-frequently-replied-users']; + post: operations['users/get-frequently-replied-users']; }; '/users/featured-notes': { /** @@ -3478,14 +3325,14 @@ export type paths = { * * **Credential required**: *No* */ - get: operations['users___featured-notes']; + get: operations['users/featured-notes']; /** * users/featured-notes * @description No description provided. * * **Credential required**: *No* */ - post: operations['users___featured-notes']; + post: operations['users/featured-notes']; }; '/users/groups/create': { /** @@ -3494,7 +3341,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___create']; + post: operations['users/groups/create']; }; '/users/groups/delete': { /** @@ -3503,7 +3350,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___delete']; + post: operations['users/groups/delete']; }; '/users/groups/invitations/accept': { /** @@ -3512,7 +3359,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___invitations___accept']; + post: operations['users/groups/invitations/accept']; }; '/users/groups/invitations/reject': { /** @@ -3521,7 +3368,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___invitations___reject']; + post: operations['users/groups/invitations/reject']; }; '/users/groups/invite': { /** @@ -3530,7 +3377,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___invite']; + post: operations['users/groups/invite']; }; '/users/groups/joined': { /** @@ -3539,7 +3386,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:user-groups* */ - post: operations['users___groups___joined']; + post: operations['users/groups/joined']; }; '/users/groups/leave': { /** @@ -3548,7 +3395,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___leave']; + post: operations['users/groups/leave']; }; '/users/groups/owned': { /** @@ -3557,7 +3404,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:user-groups* */ - post: operations['users___groups___owned']; + post: operations['users/groups/owned']; }; '/users/groups/pull': { /** @@ -3566,7 +3413,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___pull']; + post: operations['users/groups/pull']; }; '/users/groups/show': { /** @@ -3575,7 +3422,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:user-groups* */ - post: operations['users___groups___show']; + post: operations['users/groups/show']; }; '/users/groups/transfer': { /** @@ -3584,7 +3431,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___transfer']; + post: operations['users/groups/transfer']; }; '/users/groups/update': { /** @@ -3593,7 +3440,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - post: operations['users___groups___update']; + post: operations['users/groups/update']; }; '/users/lists/create': { /** @@ -3602,7 +3449,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___create']; + post: operations['users/lists/create']; }; '/users/lists/delete': { /** @@ -3611,7 +3458,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___delete']; + post: operations['users/lists/delete']; }; '/users/lists/list': { /** @@ -3620,7 +3467,7 @@ export type paths = { * * **Credential required**: *No* / **Permission**: *read:account* */ - post: operations['users___lists___list']; + post: operations['users/lists/list']; }; '/users/lists/pull': { /** @@ -3629,7 +3476,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___pull']; + post: operations['users/lists/pull']; }; '/users/lists/push': { /** @@ -3638,7 +3485,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___push']; + post: operations['users/lists/push']; }; '/users/lists/show': { /** @@ -3647,7 +3494,7 @@ export type paths = { * * **Credential required**: *No* / **Permission**: *read:account* */ - post: operations['users___lists___show']; + post: operations['users/lists/show']; }; '/users/lists/favorite': { /** @@ -3656,7 +3503,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___favorite']; + post: operations['users/lists/favorite']; }; '/users/lists/unfavorite': { /** @@ -3665,7 +3512,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___unfavorite']; + post: operations['users/lists/unfavorite']; }; '/users/lists/update': { /** @@ -3674,7 +3521,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___update']; + post: operations['users/lists/update']; }; '/users/lists/create-from-public': { /** @@ -3683,7 +3530,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___create-from-public']; + post: operations['users/lists/create-from-public']; }; '/users/lists/update-membership': { /** @@ -3692,7 +3539,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___lists___update-membership']; + post: operations['users/lists/update-membership']; }; '/users/lists/get-memberships': { /** @@ -3701,7 +3548,7 @@ export type paths = { * * **Credential required**: *No* / **Permission**: *read:account* */ - post: operations['users___lists___get-memberships']; + post: operations['users/lists/get-memberships']; }; '/users/notes': { /** @@ -3710,7 +3557,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___notes']; + post: operations['users/notes']; }; '/users/pages': { /** @@ -3719,7 +3566,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___pages']; + post: operations['users/pages']; }; '/users/flashs': { /** @@ -3728,7 +3575,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___flashs']; + post: operations['users/flashs']; }; '/users/reactions': { /** @@ -3737,7 +3584,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___reactions']; + post: operations['users/reactions']; }; '/users/recommendation': { /** @@ -3746,7 +3593,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['users___recommendation']; + post: operations['users/recommendation']; }; '/users/relation': { /** @@ -3755,7 +3602,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['users___relation']; + post: operations['users/relation']; }; '/users/report-abuse': { /** @@ -3764,7 +3611,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:report-abuse* */ - post: operations['users___report-abuse']; + post: operations['users/report-abuse']; }; '/users/search-by-username-and-host': { /** @@ -3773,7 +3620,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___search-by-username-and-host']; + post: operations['users/search-by-username-and-host']; }; '/users/search': { /** @@ -3782,7 +3629,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___search']; + post: operations['users/search']; }; '/users/show': { /** @@ -3791,7 +3638,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___show']; + post: operations['users/show']; }; '/users/stats': { /** @@ -3800,7 +3647,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___stats']; + post: operations['users/stats']; }; '/users/achievements': { /** @@ -3809,7 +3656,7 @@ export type paths = { * * **Credential required**: *No* */ - post: operations['users___achievements']; + post: operations['users/achievements']; }; '/users/update-memo': { /** @@ -3818,7 +3665,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - post: operations['users___update-memo']; + post: operations['users/update-memo']; }; '/users/translate': { /** @@ -3827,7 +3674,7 @@ export type paths = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - post: operations['users___translate']; + post: operations['users/translate']; }; '/fetch-rss': { /** @@ -3871,31 +3718,6 @@ export type paths = { */ post: operations['retention']; }; - '/bubble-game/register': { - /** - * bubble-game/register - * @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']; - }; }; export type webhooks = Record; @@ -3956,9 +3778,7 @@ export type components = { faviconUrl: string | null; themeColor: string | null; }; - emojis: { - [key: string]: string; - }; + emojis: Record; /** @enum {string} */ onlineStatus: 'unknown' | 'online' | 'active' | 'offline'; badgeRoles?: ({ @@ -3988,8 +3808,6 @@ export type components = { isSilenced: boolean; /** @example false */ isSuspended: boolean; - /** @example false */ - isSensitive: boolean; /** @example Hi masters, I am Ai! */ description: string | null; location: string | null; @@ -4051,8 +3869,6 @@ export type components = { noCrawle: boolean; preventAiLearning: boolean; isExplorable: boolean; - isIndexable: boolean; - isSensitive: boolean; isDeleted: boolean; /** @enum {string} */ twoFactorBackupCodesStock: 'full' | 'partial' | 'none'; @@ -4071,141 +3887,46 @@ export type components = { hardMutedWords: string[][]; mutedInstances: string[] | null; 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<[{ + app?: { /** @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; - }]>; - groupInvited?: OneOf<[{ - /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; - }, { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + quote?: { /** @enum {string} */ - type: 'list'; - /** Format: misskey:id */ - userListId: string; - }]>; - roleAssigned?: OneOf<[{ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + reply?: { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; - }, { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + follow?: { /** @enum {string} */ - type: 'list'; - /** Format: misskey:id */ - userListId: string; - }]>; - achievementEarned?: OneOf<[{ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + renote?: { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; - }, { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + mention?: { /** @enum {string} */ - type: 'list'; - /** Format: misskey:id */ - userListId: string; - }]>; - app?: OneOf<[{ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + reaction?: { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; - }, { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + pollEnded?: { /** @enum {string} */ - type: 'list'; - /** Format: misskey:id */ - userListId: string; - }]>; - test?: OneOf<[{ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + receiveFollowRequest?: { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; - }, { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + groupInvited?: { /** @enum {string} */ - type: 'list'; - /** Format: misskey:id */ - userListId: string; - }]>; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; }; emailNotificationTypes: string[]; achievements: { @@ -4213,7 +3934,32 @@ export type components = { unlockedAt: number; }[]; loggedInDays: number; - policies: components['schemas']['RolePolicies']; + policies: { + gtlAvailable: boolean; + ltlAvailable: boolean; + canPublicNote: boolean; + canInvite: boolean; + inviteLimit: number; + inviteLimitCycle: number; + inviteExpirationTime: number; + canManageCustomEmojis: boolean; + canManageAvatarDecorations: boolean; + canSearchNotes: boolean; + canUseTranslator: boolean; + canHideAds: boolean; + driveCapacityMb: number; + alwaysMarkNsfw: boolean; + pinLimit: number; + antennaLimit: number; + wordMuteLimit: number; + webhookLimit: number; + clipLimit: number; + noteEachClipsLimit: number; + userListLimit: number; + userEachUserListsLimit: number; + rateLimitFactor: number; + avatarDecorationLimit: number; + }; email?: string | null; emailVerified?: boolean | null; securityKeysList?: { @@ -4230,7 +3976,7 @@ export type components = { UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly']; MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly']; UserDetailed: components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed']; - User: components['schemas']['UserLite'] | components['schemas']['UserDetailed']; + User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'] | components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed']; UserList: { /** * Format: id @@ -4287,10 +4033,8 @@ export type components = { text: string; title: string; imageUrl: string | null; - /** @enum {string} */ - icon: 'info' | 'warning' | 'error' | 'success'; - /** @enum {string} */ - display: 'dialog' | 'normal' | 'banner'; + icon: string; + display: string; needConfirmationToRead: boolean; silence: boolean; forYou: boolean; @@ -4361,59 +4105,36 @@ export type components = { renote?: components['schemas']['Note'] | null; disableRightClick?: boolean; isHidden?: boolean; - /** @enum {string} */ - visibility: 'public' | 'home' | 'followers' | 'specified' | 'private'; + visibility: string; mentions?: string[]; visibleUserIds?: string[]; fileIds?: string[]; files?: components['schemas']['DriveFile'][]; tags?: string[]; - poll?: ({ - /** Format: date-time */ - expiresAt?: string | null; - multiple: boolean; - choices: { - isVoted: boolean; - text: string; - votes: number; - }[]; - }) | null; - /** Format: date-time */ - deleteAt?: string | null; - emojis?: { - [key: string]: string; - }; - event?: Record | null; + poll?: Record | null; + event?: Record | null; /** * Format: id * @example xxxxxxxxxx */ channelId?: string | null; - channel?: ({ + channel?: { id: string; name: string; color: string; isSensitive: boolean; allowRenoteToExternal: boolean; - userId: string | null; - }) | null; + } | null; localOnly?: boolean; - /** @enum {string|null} */ - reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; - reactionEmojis: { - [key: string]: string; - }; - reactions: { - [key: string]: number; - }; - reactionCount: number; + reactionAcceptance: string | null; + reactions: Record; renoteCount: number; repliesCount: number; uri?: string; url?: string; reactionAndUserPairCache?: string[]; clippedCount?: number; - myReaction?: string | null; + myReaction?: Record | null; }; NoteReaction: { /** @@ -4444,226 +4165,76 @@ export type components = { /** Format: date-time */ createdAt: string; /** @enum {string} */ - type: 'note'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - note: components['schemas']['Note']; - } | { + type: 'note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'groupInvited' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped'; + user?: components['schemas']['UserLite'] | null; /** Format: id */ + userId?: string | null; + note?: components['schemas']['Note'] | null; + reaction?: string | null; + achievement?: string; + body?: string | null; + header?: string | null; + icon?: string | null; + reactions?: { + user: components['schemas']['UserLite']; + reaction: string; + }[] | null; + users?: components['schemas']['UserLite'][] | null; + }; + DriveFile: { + /** + * Format: id + * @example xxxxxxxxxx + */ id: string; /** Format: date-time */ createdAt: string; - /** @enum {string} */ - type: 'mention'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - note: components['schemas']['Note']; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'reply'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - note: components['schemas']['Note']; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'renote'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - note: components['schemas']['Note']; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'quote'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - note: components['schemas']['Note']; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'reaction'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - note: components['schemas']['Note']; - reaction: string; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'pollEnded'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - note: components['schemas']['Note']; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'follow'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'receiveFollowRequest'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'followRequestAccepted'; - user: components['schemas']['UserLite']; - /** Format: id */ - userId: string; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'roleAssigned'; - role: components['schemas']['Role']; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'achievementEarned'; - achievement: string; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'app'; - body: string; - header: string; - icon: string; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'reaction:grouped'; - note: components['schemas']['Note']; - reactions: { - user: components['schemas']['UserLite']; - reaction: string; - }[]; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'renote:grouped'; - note: components['schemas']['Note']; - users: components['schemas']['UserLite'][]; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'test'; - } | { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - type: 'groupInvited'; - /** Format: id */ - invitation: string; - }; - DriveFile: { - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: date-time */ - createdAt: string; - /** @example 192.jpg */ - name: string; - /** @example image/jpeg */ - type: string; - /** - * Format: md5 - * @example 15eca7fba0480996e2245f5185bf39f2 - */ - md5: string; - /** @example 51469 */ - size: number; - isSensitive: boolean; - blurhash: string | null; - properties: { - /** @example 1280 */ - width?: number; - /** @example 720 */ - height?: number; - /** @example 8 */ - orientation?: number; - /** @example rgb(40,65,87) */ - avgColor?: string; - }; - /** Format: url */ - url: string; - /** Format: url */ - thumbnailUrl: string | null; - comment: string | null; - /** - * Format: id - * @example xxxxxxxxxx - */ - folderId: string | null; - folder?: components['schemas']['DriveFolder'] | null; - /** - * Format: id - * @example xxxxxxxxxx - */ - userId: string | null; - user?: components['schemas']['UserLite'] | null; - }; - DriveFolder: { - /** - * Format: id - * @example xxxxxxxxxx - */ + /** @example lenna.jpg */ + name: string; + /** @example image/jpeg */ + type: string; + /** + * Format: md5 + * @example 15eca7fba0480996e2245f5185bf39f2 + */ + md5: string; + /** @example 51469 */ + size: number; + isSensitive: boolean; + blurhash: string | null; + properties: { + /** @example 1280 */ + width?: number; + /** @example 720 */ + height?: number; + /** @example 8 */ + orientation?: number; + /** @example rgb(40,65,87) */ + avgColor?: string; + }; + /** Format: url */ + url: string; + /** Format: url */ + thumbnailUrl: string | null; + comment: string | null; + /** + * Format: id + * @example xxxxxxxxxx + */ + folderId: string | null; + folder?: components['schemas']['DriveFolder'] | null; + /** + * Format: id + * @example xxxxxxxxxx + */ + userId: string | null; + user?: components['schemas']['UserLite'] | null; + }; + DriveFolder: { + /** + * Format: id + * @example xxxxxxxxxx + */ id: string; /** Format: date-time */ createdAt: string; @@ -4689,8 +4260,8 @@ export type components = { followeeId: string; /** Format: id */ followerId: string; - followee?: components['schemas']['UserDetailedNotMe']; - follower?: components['schemas']['UserDetailedNotMe']; + followee?: components['schemas']['UserDetailed']; + follower?: components['schemas']['UserDetailed']; }; Muting: { /** @@ -4704,7 +4275,7 @@ export type components = { expiresAt: string | null; /** Format: id */ muteeId: string; - mutee: components['schemas']['UserDetailedNotMe']; + mutee: components['schemas']['UserDetailed']; }; RenoteMuting: { /** @@ -4716,7 +4287,7 @@ export type components = { createdAt: string; /** Format: id */ muteeId: string; - mutee: components['schemas']['UserDetailedNotMe']; + mutee: components['schemas']['UserDetailed']; }; Blocking: { /** @@ -4728,7 +4299,7 @@ export type components = { createdAt: string; /** Format: id */ blockeeId: string; - blockee: components['schemas']['UserDetailedNotMe']; + blockee: components['schemas']['UserDetailed']; }; Hashtag: { /** @example cherrypick */ @@ -4771,7 +4342,7 @@ export type components = { /** Format: id */ userId: string; user: components['schemas']['UserLite']; - content: components['schemas']['PageBlock'][]; + content: Record[]; variables: Record[]; title: string; name: string; @@ -4786,29 +4357,6 @@ export type components = { likedCount: number; isLiked?: boolean; }; - PageBlock: OneOf<[{ - id: string; - /** @enum {string} */ - type: 'text'; - text: string; - }, { - id: string; - /** @enum {string} */ - type: 'section'; - title: string; - children: components['schemas']['PageBlock'][]; - }, { - id: string; - /** @enum {string} */ - type: 'image'; - fileId: string | null; - }, { - id: string; - /** @enum {string} */ - type: 'note'; - detailed: boolean; - note: string | null; - }]>; Channel: { /** * Format: id @@ -4862,16 +4410,13 @@ export type components = { caseSensitive: boolean; /** @default false */ localOnly: boolean; - /** @default false */ - excludeBots: boolean; + notify: boolean; /** @default false */ withReplies: boolean; withFile: boolean; isActive: boolean; /** @default false */ hasUnreadNote: boolean; - /** @default false */ - notify: boolean; }; Clip: { /** @@ -4891,7 +4436,6 @@ export type components = { isPublic: boolean; favoritedCount: number; isFavorited?: boolean; - notesCount?: number; }; FederationInstance: { /** Format: id */ @@ -4906,8 +4450,6 @@ export type components = { followersCount: number; isNotResponding: boolean; isSuspended: boolean; - /** @enum {string} */ - suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'; isBlocked: boolean; /** @example cherrypick */ softwareName: string | null; @@ -4919,7 +4461,6 @@ export type components = { maintainerName: string | null; maintainerEmail: string | null; isSilenced: boolean; - isMediaSilenced: boolean; /** Format: url */ iconUrl: string | null; /** Format: url */ @@ -4929,7 +4470,6 @@ export type components = { infoUpdatedAt: string | null; /** Format: date-time */ latestRequestReceivedAt: string | null; - moderationNote?: string | null; }; GalleryPost: { /** @@ -4958,7 +4498,6 @@ export type components = { name: string; category: string | null; url: string; - localOnly?: boolean; isSensitive?: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; }; @@ -5003,51 +4542,6 @@ export type components = { headers: Record; success: boolean; }; - RoleCondFormulaLogics: { - id: string; - /** @enum {string} */ - type: 'and' | 'or'; - values: components['schemas']['RoleCondFormulaValue'][]; - }; - RoleCondFormulaValueNot: { - id: string; - /** @enum {string} */ - type: 'not'; - value: components['schemas']['RoleCondFormulaValue']; - }; - RoleCondFormulaValueIsLocalOrRemote: { - id: string; - /** @enum {string} */ - type: 'isLocal' | 'isRemote'; - }; - RoleCondFormulaValueUserSettingBooleanSchema: { - id: string; - /** @enum {string} */ - type: 'isSuspended' | 'isLocked' | 'isBot' | 'isCat' | 'isExplorable'; - }; - RoleCondFormulaValueAssignedRole: { - id: string; - /** @enum {string} */ - type: 'roleAssignedTo'; - /** - * Format: id - * @example xxxxxxxxxx - */ - roleId: string; - }; - RoleCondFormulaValueCreated: { - id: string; - /** @enum {string} */ - type: 'createdLessThan' | 'createdMoreThan'; - sec: number; - }; - RoleCondFormulaFollowersOrFollowingOrNotes: { - id: string; - /** @enum {string} */ - type: 'followersLessThanOrEq' | 'followersMoreThanOrEq' | 'followingLessThanOrEq' | 'followingMoreThanOrEq' | 'notesLessThanOrEq' | 'notesMoreThanOrEq'; - value: number; - }; - RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueUserSettingBooleanSchema'] | components['schemas']['RoleCondFormulaValueAssignedRole'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; RoleLite: { /** * Format: id @@ -5074,7 +4568,7 @@ export type components = { updatedAt: string; /** @enum {string} */ target: 'manual' | 'conditional'; - condFormula: components['schemas']['RoleCondFormulaValue']; + condFormula: Record; /** @example false */ isPublic: boolean; /** @example false */ @@ -5084,170 +4578,129 @@ export type components = { /** @example false */ canEditMembersByModerator: boolean; policies: { - [key: string]: { - value?: number | boolean; - priority?: number; - useDefault?: boolean; + pinLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canInvite: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + clipLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canHideAds: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + inviteLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + antennaLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + gtlAvailable: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + ltlAvailable: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + webhookLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canPublicNote: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + userListLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + wordMuteLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + alwaysMarkNsfw: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canSearchNotes: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + driveCapacityMb: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + rateLimitFactor: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + inviteLimitCycle: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + noteEachClipsLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + inviteExpirationTime: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canManageCustomEmojis: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + userEachUserListsLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canManageAvatarDecorations: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + canUseTranslator: { + value: number | boolean; + priority: number; + useDefault: boolean; + }; + avatarDecorationLimit: { + value: number | boolean; + priority: number; + useDefault: boolean; }; }; usersCount: number; }); - RolePolicies: { - gtlAvailable: boolean; - ltlAvailable: boolean; - canPublicNote: boolean; - mentionLimit: number; - canInvite: boolean; - inviteLimit: number; - inviteLimitCycle: number; - inviteExpirationTime: number; - canManageCustomEmojis: boolean; - canManageAvatarDecorations: boolean; - canSearchNotes: boolean; - canAdvancedSearchNotes: boolean; - canUseTranslator: boolean; - canHideAds: boolean; - driveCapacityMb: number; - alwaysMarkNsfw: boolean; - canUpdateBioMedia: boolean; - pinLimit: number; - antennaLimit: number; - wordMuteLimit: number; - webhookLimit: number; - clipLimit: number; - noteEachClipsLimit: number; - userListLimit: number; - userEachUserListsLimit: number; - rateLimitFactor: number; - avatarDecorationLimit: number; - fileSizeLimit: number; - canEditNote: boolean; - }; - MetaLite: { - maintainerName: string | null; - maintainerEmail: string | null; - version: string; - basedMisskeyVersion: string; - providesTarball: boolean; - name: string | null; - shortName: string | null; - /** - * Format: url - * @example https://cherrypick.example.com - */ - uri: string; - description: string | null; - langs: string[]; - tosUrl: string | null; - /** @default https://github.com/kokonect-link/cherrypick */ - repositoryUrl: string | null; - /** @default https://github.com/kokonect-link/cherrypick/issues/new */ - feedbackUrl: string | null; - defaultDarkTheme: string | null; - defaultLightTheme: string | null; - disableRegistration: boolean; - emailRequiredForSignup: boolean; - enableHcaptcha: boolean; - hcaptchaSiteKey: string | null; - enableMcaptcha: boolean; - mcaptchaSiteKey: string | null; - mcaptchaInstanceUrl: string | null; - enableRecaptcha: boolean; - recaptchaSiteKey: string | null; - enableTurnstile: boolean; - turnstileSiteKey: string | null; - swPublickey: string | null; - /** @default /assets/ai.png */ - mascotImageUrl: string; - bannerUrl: string | null; - serverErrorImageUrl: string | null; - infoImageUrl: string | null; - notFoundImageUrl: string | null; - iconUrl: string | null; - maxNoteTextLength: number; - ads: { - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: url */ - url: string; - place: string; - ratio: number; - /** Format: url */ - imageUrl: string; - dayOfWeek: number; - }[]; - /** @default 0 */ - notesPerOneAd: number; - enableEmail: boolean; - enableServiceWorker: boolean; - translatorAvailable: boolean; - mediaProxy: string; - enableUrlPreview: boolean; - urlPreviewEndpoint: string; - backgroundImageUrl: string | null; - impressumUrl: string | null; - logoImageUrl: string | null; - privacyPolicyUrl: string | null; - inquiryUrl: string | null; - serverRules: string[]; - themeColor: string | null; - policies: components['schemas']['RolePolicies']; - /** - * @default local - * @enum {string} - */ - noteSearchableScope: 'local' | 'global'; - }; - MetaDetailedOnly: { - features?: { - registration: boolean; - emailRequiredForSignup: boolean; - localTimeline: boolean; - globalTimeline: boolean; - hcaptcha: boolean; - turnstile: boolean; - recaptcha: boolean; - objectStorage: boolean; - serviceWorker: boolean; - /** @default true */ - miauth?: boolean; - }; - proxyAccountName: string | null; - /** @example false */ - requireSetup: boolean; - cacheRemoteFiles: boolean; - cacheRemoteSensitiveFiles: boolean; - }; - MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly']; - SystemWebhook: { - id: string; - isActive: boolean; - /** Format: date-time */ - updatedAt: string; - /** Format: date-time */ - latestSentAt: string | null; - latestStatus: number | null; - name: string; - on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; - url: string; - secret: string; - }; - AbuseReportNotificationRecipient: { - id: string; - isActive: boolean; - /** Format: date-time */ - updatedAt: string; - name: string; - /** @enum {string} */ - method: 'email' | 'webhook'; - userId?: string; - user?: components['schemas']['UserLite']; - systemWebhookId?: string; - systemWebhook?: components['schemas']['SystemWebhook']; - }; }; responses: never; parameters: never; @@ -5268,7 +4721,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:meta* */ - admin___meta: { + 'admin/meta': { responses: { /** @description OK (with results) */ 200: { @@ -5279,9 +4732,6 @@ export type operations = { emailRequiredForSignup: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; - enableMcaptcha: boolean; - mcaptchaSiteKey: string | null; - mcaptchaInstanceUrl: string | null; enableRecaptcha: boolean; recaptchaSiteKey: string | null; enableTurnstile: boolean; @@ -5301,16 +4751,13 @@ export type operations = { translatorAvailable: boolean; translatorType: string | null; silencedHosts?: string[]; - mediaSilencedHosts: string[]; pinnedUsers: string[]; hiddenTags: string[]; blockedHosts: string[]; sensitiveWords: string[]; - prohibitedWords: string[]; bannedEmailDomains?: string[]; preservedUsernames: string[]; hcaptchaSecretKey: string | null; - mcaptchaSecretKey: string | null; recaptchaSecretKey: string | null; turnstileSecretKey: string | null; sensitiveMediaDetection: string; @@ -5354,9 +4801,6 @@ export type operations = { enableActiveEmailValidation: boolean; enableVerifymailApi: boolean; verifymailAuthKey: string | null; - enableTruemailApi: boolean; - truemailInstance: string | null; - truemailAuthKey: string | null; enableChartsForRemoteUser: boolean; enableChartsForFederatedInstances: boolean; enableServerMachineStats: boolean; @@ -5385,23 +4829,12 @@ export type operations = { objectStorageS3ForcePathStyle: boolean; objectStorageRemoteS3ForcePathStyle: boolean; privacyPolicyUrl: string | null; - inquiryUrl: string | null; - repositoryUrl: string | null; - /** - * @deprecated - * @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. - */ + repositoryUrl: string; 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; doNotSendNotificationEmailsForAbuseReport: boolean; emailToReceiveAbuseReport: string | null; enableReceivePrerelease: boolean; @@ -5446,10 +4879,9 @@ export type operations = { * admin/abuse-report-resolver/create * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-create* + * **Credential required**: *Yes* */ - 'admin___abuse-report-resolver___create': { + 'admin/abuse-report-resolver/create': { requestBody: { content: { 'application/json': { @@ -5513,10 +4945,9 @@ export type operations = { * admin/abuse-report-resolver/list * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-list* + * **Credential required**: *Yes* */ - 'admin___abuse-report-resolver___list': { + 'admin/abuse-report-resolver/list': { requestBody: { content: { 'application/json': { @@ -5579,10 +5010,9 @@ export type operations = { * admin/abuse-report-resolver/delete * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *No* / **Permission**: *arr-delete* + * **Credential required**: *No* */ - 'admin___abuse-report-resolver___delete': { + 'admin/abuse-report-resolver/delete': { requestBody: { content: { 'application/json': { @@ -5632,10 +5062,9 @@ export type operations = { * admin/abuse-report-resolver/update * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *arr-update* + * **Credential required**: *Yes* */ - 'admin___abuse-report-resolver___update': { + 'admin/abuse-report-resolver/update': { requestBody: { content: { 'application/json': { @@ -5694,7 +5123,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports* */ - 'admin___abuse-user-reports': { + 'admin/abuse-user-reports': { requestBody: { content: { 'application/json': { @@ -5742,9 +5171,9 @@ export type operations = { targetUserId: string; /** Format: id */ assigneeId: string | null; - reporter: components['schemas']['UserDetailedNotMe']; - targetUser: components['schemas']['UserDetailedNotMe']; - assignee?: components['schemas']['UserDetailedNotMe'] | null; + reporter: components['schemas']['User']; + targetUser: components['schemas']['User']; + assignee?: components['schemas']['User'] | null; })[]; }; }; @@ -5781,17 +5210,17 @@ export type operations = { }; }; /** - * admin/abuse-report/notification-recipient/list + * admin/accounts/create * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient* + * **Credential required**: *No* */ - 'admin___abuse-report___notification-recipient___list': { + 'admin/accounts/create': { requestBody: { content: { 'application/json': { - method?: ('email' | 'webhook')[]; + username: string; + password: string; }; }; }; @@ -5799,7 +5228,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['AbuseReportNotificationRecipient'][]; + 'application/json': components['schemas']['User']; }; }; /** @description Client error */ @@ -5835,27 +5264,24 @@ export type operations = { }; }; /** - * admin/abuse-report/notification-recipient/show + * admin/accounts/delete * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient* + * **Credential required**: *Yes* / **Permission**: *write:admin:account* */ - 'admin___abuse-report___notification-recipient___show': { + 'admin/accounts/delete': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - id: string; + userId: string; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['AbuseReportNotificationRecipient']; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -5890,24 +5316,16 @@ export type operations = { }; }; /** - * admin/abuse-report/notification-recipient/create + * admin/accounts/find-by-email * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick 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:account* */ - 'admin___abuse-report___notification-recipient___create': { + 'admin/accounts/find-by-email': { requestBody: { content: { 'application/json': { - isActive: boolean; - name: string; - /** @enum {string} */ - method: 'email' | 'webhook'; - /** Format: misskey:id */ - userId?: string; - /** Format: misskey:id */ - systemWebhookId?: string; + email: string; }; }; }; @@ -5915,7 +5333,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['AbuseReportNotificationRecipient']; + 'application/json': components['schemas']['User']; }; }; /** @description Client error */ @@ -5951,26 +5369,24 @@ export type operations = { }; }; /** - * admin/abuse-report/notification-recipient/update + * admin/ad/create * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* + * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - 'admin___abuse-report___notification-recipient___update': { + 'admin/ad/create': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - id: string; - isActive: boolean; - name: string; - /** @enum {string} */ - method: 'email' | 'webhook'; - /** Format: misskey:id */ - userId?: string; - /** Format: misskey:id */ - systemWebhookId?: string; + url: string; + memo: string; + place: string; + priority: string; + ratio: number; + expiresAt: number; + startsAt: number; + imageUrl: string; + dayOfWeek: number; }; }; }; @@ -5978,7 +5394,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['AbuseReportNotificationRecipient']; + 'application/json': components['schemas']['Ad']; }; }; /** @description Client error */ @@ -6014,13 +5430,12 @@ export type operations = { }; }; /** - * admin/abuse-report/notification-recipient/delete + * admin/ad/delete * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* + * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - 'admin___abuse-report___notification-recipient___delete': { + 'admin/ad/delete': { requestBody: { content: { 'application/json': { @@ -6067,17 +5482,23 @@ export type operations = { }; }; /** - * admin/accounts/create + * admin/ad/list * @description No description provided. * - * **Credential required**: *No* + * **Credential required**: *Yes* / **Permission**: *read:admin:ad* */ - admin___accounts___create: { + 'admin/ad/list': { requestBody: { content: { 'application/json': { - username: string; - password: string; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default null */ + publishing?: boolean | null; }; }; }; @@ -6085,7 +5506,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['MeDetailed']; + 'application/json': components['schemas']['Ad'][]; }; }; /** @description Client error */ @@ -6121,17 +5542,26 @@ export type operations = { }; }; /** - * admin/accounts/delete + * admin/ad/update * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:account* + * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - admin___accounts___delete: { + 'admin/ad/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - userId: string; + id: string; + memo: string; + url: string; + imageUrl: string; + place: string; + priority: string; + ratio: number; + expiresAt: number; + startsAt: number; + dayOfWeek: number; }; }; }; @@ -6173,16 +5603,39 @@ export type operations = { }; }; /** - * admin/accounts/find-by-email + * admin/announcements/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:account* + * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - 'admin___accounts___find-by-email': { + 'admin/announcements/create': { requestBody: { content: { 'application/json': { - email: string; + title: string; + text: string; + imageUrl: string | null; + /** + * @default info + * @enum {string} + */ + icon?: 'info' | 'warning' | 'error' | 'success'; + /** + * @default normal + * @enum {string} + */ + display?: 'normal' | 'banner' | 'dialog'; + /** @default false */ + forExistingUsers?: boolean; + /** @default false */ + silence?: boolean; + /** @default false */ + needConfirmationToRead?: boolean; + /** + * Format: misskey:id + * @default null + */ + userId?: string | null; }; }; }; @@ -6190,7 +5643,20 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['UserDetailedNotMe']; + 'application/json': { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + title: string; + text: string; + imageUrl: string | null; + }; }; }; /** @description Client error */ @@ -6226,24 +5692,75 @@ export type operations = { }; }; /** - * admin/ad/create + * admin/announcements/delete * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:ad* + * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - admin___ad___create: { + 'admin/announcements/delete': { requestBody: { content: { 'application/json': { - url: string; - memo: string; - place: string; - priority: string; - ratio: number; - expiresAt: number; - startsAt: number; - imageUrl: string; - dayOfWeek: number; + /** Format: misskey:id */ + id: 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/announcements/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:announcements* + */ + 'admin/announcements/list': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + userId?: string | null; }; }; }; @@ -6251,7 +5768,21 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['Ad']; + 'application/json': ({ + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + text: string; + title: string; + imageUrl: string | null; + reads: number; + })[]; }; }; /** @description Client error */ @@ -6287,17 +5818,28 @@ export type operations = { }; }; /** - * admin/ad/delete + * admin/announcements/update * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:ad* + * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - admin___ad___delete: { + 'admin/announcements/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ id: string; + title?: string; + text?: string; + imageUrl?: string | null; + /** @enum {string} */ + icon?: 'info' | 'warning' | 'error' | 'success'; + /** @enum {string} */ + display?: 'normal' | 'banner' | 'dialog'; + forExistingUsers?: boolean; + silence?: boolean; + needConfirmationToRead?: boolean; + isActive?: boolean; }; }; }; @@ -6339,32 +5881,26 @@ export type operations = { }; }; /** - * admin/ad/list + * admin/avatar-decorations/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:ad* + * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - admin___ad___list: { + 'admin/avatar-decorations/create': { requestBody: { content: { 'application/json': { - /** @default 10 */ - limit?: number; - /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; - /** @default null */ - publishing?: boolean | null; + name: string; + description: string; + url: string; + roleIdsThatCanBeUsedThisDecoration?: string[]; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['Ad'][]; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -6399,26 +5935,17 @@ export type operations = { }; }; /** - * admin/ad/update + * admin/avatar-decorations/delete * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:ad* + * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - admin___ad___update: { + 'admin/avatar-decorations/delete': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ id: string; - memo?: string; - url?: string; - imageUrl?: string; - place?: string; - priority?: string; - ratio?: number; - expiresAt?: number; - startsAt?: number; - dayOfWeek?: number; }; }; }; @@ -6460,38 +5987,22 @@ export type operations = { }; }; /** - * admin/announcements/create + * admin/avatar-decorations/list * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* + * **Credential required**: *Yes* / **Permission**: *read:admin:avatar-decorations* */ - admin___announcements___create: { + 'admin/avatar-decorations/list': { requestBody: { content: { 'application/json': { - title: string; - text: string; - imageUrl: string | null; - /** - * @default info - * @enum {string} - */ - icon?: 'info' | 'warning' | 'error' | 'success'; - /** - * @default normal - * @enum {string} - */ - display?: 'normal' | 'banner' | 'dialog'; - /** @default false */ - forExistingUsers?: boolean; - /** @default false */ - silence?: boolean; - /** @default false */ - needConfirmationToRead?: boolean; - /** - * Format: misskey:id - * @default null - */ + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ userId?: string | null; }; }; @@ -6500,20 +6011,21 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string | null; - title: string; - text: string; - imageUrl: string | null; - }; + 'application/json': ({ + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + name: string; + description: string; + url: string; + roleIdsThatCanBeUsedThisDecoration: string[]; + })[]; }; }; /** @description Client error */ @@ -6549,17 +6061,21 @@ export type operations = { }; }; /** - * admin/announcements/delete + * admin/avatar-decorations/update * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* + * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - admin___announcements___delete: { + 'admin/avatar-decorations/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ id: string; + name?: string; + description?: string; + url?: string; + roleIdsThatCanBeUsedThisDecoration?: string[]; }; }; }; @@ -6601,51 +6117,24 @@ export type operations = { }; }; /** - * admin/announcements/list + * admin/delete-all-files-of-a-user * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:announcements* + * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user* */ - admin___announcements___list: { + 'admin/delete-all-files-of-a-user': { requestBody: { content: { 'application/json': { - /** @default 10 */ - limit?: number; /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; - /** Format: misskey:id */ - userId?: string | null; - /** - * @default active - * @enum {string} - */ - status?: 'all' | 'active' | 'archived'; + userId: string; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': ({ - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string | null; - text: string; - title: string; - imageUrl: string | null; - reads: number; - })[]; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -6680,28 +6169,17 @@ export type operations = { }; }; /** - * admin/announcements/update + * admin/unset-user-avatar * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* + * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar* */ - admin___announcements___update: { + 'admin/unset-user-avatar': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - id: string; - title?: string; - text?: string; - imageUrl?: string | null; - /** @enum {string} */ - icon?: 'info' | 'warning' | 'error' | 'success'; - /** @enum {string} */ - display?: 'normal' | 'banner' | 'dialog'; - forExistingUsers?: boolean; - silence?: boolean; - needConfirmationToRead?: boolean; - isActive?: boolean; + userId: string; }; }; }; @@ -6743,19 +6221,17 @@ export type operations = { }; }; /** - * admin/avatar-decorations/create + * admin/unset-user-banner * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* + * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner* */ - 'admin___avatar-decorations___create': { + 'admin/unset-user-banner': { requestBody: { content: { 'application/json': { - name: string; - description: string; - url: string; - roleIdsThatCanBeUsedThisDecoration?: string[]; + /** Format: misskey:id */ + userId: string; }; }; }; @@ -6797,20 +6273,12 @@ export type operations = { }; }; /** - * admin/avatar-decorations/delete + * admin/drive/clean-remote-files * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* + * **Credential required**: *Yes* / **Permission**: *write:admin:drive* */ - 'admin___avatar-decorations___delete': { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - id: string; - }; - }; - }; + 'admin/drive/clean-remote-files': { responses: { /** @description OK (without any results) */ 204: { @@ -6849,46 +6317,16 @@ export type operations = { }; }; /** - * admin/avatar-decorations/list + * admin/drive/cleanup * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:avatar-decorations* + * **Credential required**: *Yes* / **Permission**: *write:admin:drive* */ - 'admin___avatar-decorations___list': { - requestBody: { - content: { - 'application/json': { - /** @default 10 */ - limit?: number; - /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; - /** Format: misskey:id */ - userId?: string | null; - }; - }; - }; + 'admin/drive/cleanup': { responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': ({ - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string | null; - name: string; - description: string; - url: string; - roleIdsThatCanBeUsedThisDecoration: string[]; - })[]; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -6923,28 +6361,43 @@ export type operations = { }; }; /** - * admin/avatar-decorations/update + * admin/drive/files * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* + * **Credential required**: *Yes* / **Permission**: *read:admin:drive* */ - 'admin___avatar-decorations___update': { + 'admin/drive/files': { requestBody: { content: { 'application/json': { + /** @default 10 */ + limit?: number; /** Format: misskey:id */ - id: string; - name?: string; - description?: string; - url?: string; - roleIdsThatCanBeUsedThisDecoration?: string[]; + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + userId?: string | null; + type?: string | null; + /** + * @default local + * @enum {string} + */ + origin?: 'combined' | 'local' | 'remote'; + /** + * @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']['DriveFile'][]; + }; }; /** @description Client error */ 400: { @@ -6979,77 +6432,77 @@ export type operations = { }; }; /** - * admin/delete-all-files-of-a-user + * admin/drive/show-file * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user* + * **Credential required**: *Yes* / **Permission**: *read:admin:drive* */ - 'admin___delete-all-files-of-a-user': { + 'admin/drive/show-file': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - userId: string; + fileId?: string; + url?: 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: { + /** @description OK (with results) */ + 200: { content: { - 'application/json': components['schemas']['Error']; - }; - }; - }; - }; - /** - * admin/unset-user-avatar - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar* - */ - 'admin___unset-user-avatar': { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - userId: string; + 'application/json': { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** + * Format: id + * @example xxxxxxxxxx + */ + userId: string | null; + /** @description The local host is represented with `null`. */ + userHost: string | null; + /** + * Format: md5 + * @example 15eca7fba0480996e2245f5185bf39f2 + */ + md5: string; + /** @example lenna.jpg */ + name: string; + /** @example image/jpeg */ + type: string; + /** @example 51469 */ + size: number; + comment: string | null; + blurhash: string | null; + properties: Record; + /** @example true */ + storedInternal: boolean | null; + /** Format: url */ + url: string | null; + /** Format: url */ + thumbnailUrl: string | null; + /** Format: url */ + webpublicUrl: string | null; + accessKey: string | null; + thumbnailAccessKey: string | null; + webpublicAccessKey: string | null; + uri: string | null; + src: string | null; + /** + * Format: id + * @example xxxxxxxxxx + */ + folderId: string | null; + isSensitive: boolean; + isLink: boolean; + }; }; }; - }; - responses: { - /** @description OK (without any results) */ - 204: { - content: never; - }; /** @description Client error */ 400: { content: { @@ -7083,17 +6536,17 @@ export type operations = { }; }; /** - * admin/unset-user-banner + * admin/emoji/add-aliases-bulk * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner* + * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - 'admin___unset-user-banner': { + 'admin/emoji/add-aliases-bulk': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - userId: string; + ids: string[]; + aliases: string[]; }; }; }; @@ -7135,12 +6588,28 @@ export type operations = { }; }; /** - * admin/drive/clean-remote-files + * admin/emoji/add * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:drive* + * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - 'admin___drive___clean-remote-files': { + 'admin/emoji/add': { + 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[]; + }; + }; + }; responses: { /** @description OK (without any results) */ 204: { @@ -7179,12 +6648,28 @@ export type operations = { }; }; /** - * admin/drive/cleanup + * admin/emoji/adds * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:drive* + * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - admin___drive___cleanup: { + 'admin/emoji/adds': { + 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[]; + }; + }; + }; responses: { /** @description OK (without any results) */ 204: { @@ -7223,34 +6708,17 @@ export type operations = { }; }; /** - * admin/drive/files + * admin/emoji/copy * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:drive* + * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - admin___drive___files: { + 'admin/emoji/copy': { requestBody: { content: { 'application/json': { - /** @default 10 */ - limit?: number; - /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; /** Format: misskey:id */ - userId?: string | null; - type?: string | null; - /** - * @default local - * @enum {string} - */ - origin?: 'combined' | 'local' | 'remote'; - /** - * @description The local host is represented with `null`. - * @default null - */ - hostname?: string | null; + emojiId: string; }; }; }; @@ -7258,7 +6726,10 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['DriveFile'][]; + 'application/json': { + /** Format: id */ + id: string; + }; }; }; /** @description Client error */ @@ -7294,81 +6765,23 @@ export type operations = { }; }; /** - * admin/drive/show-file + * admin/emoji/delete-bulk * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:drive* + * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - 'admin___drive___show-file': { + 'admin/emoji/delete-bulk': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - fileId?: string; - url?: string; + ids: string[]; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': { - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: date-time */ - createdAt: string; - /** - * Format: id - * @example xxxxxxxxxx - */ - userId: string | null; - /** @description The local host is represented with `null`. */ - userHost: string | null; - /** - * Format: md5 - * @example 15eca7fba0480996e2245f5185bf39f2 - */ - md5: string; - /** @example 192.jpg */ - name: string; - /** @example image/jpeg */ - type: string; - /** @example 51469 */ - size: number; - comment: string | null; - blurhash: string | null; - properties: { - width?: number; - height?: number; - orientation?: number; - avgColor?: string; - }; - /** @example true */ - storedInternal: boolean | null; - /** Format: url */ - url: string | null; - /** Format: url */ - thumbnailUrl: string | null; - /** Format: url */ - webpublicUrl: string | null; - accessKey: string | null; - thumbnailAccessKey: string | null; - webpublicAccessKey: string | null; - uri: string | null; - src: string | null; - /** - * Format: id - * @example xxxxxxxxxx - */ - folderId: string | null; - isSensitive: boolean; - isLink: boolean; - }; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -7403,17 +6816,17 @@ export type operations = { }; }; /** - * admin/emoji/add-aliases-bulk + * admin/emoji/delete * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - 'admin___emoji___add-aliases-bulk': { + 'admin/emoji/delete': { requestBody: { content: { 'application/json': { - ids: string[]; - aliases: string[]; + /** Format: misskey:id */ + id: string; }; }; }; @@ -7455,34 +6868,25 @@ export type operations = { }; }; /** - * admin/emoji/add + * admin/emoji/import-zip * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* + * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* */ - admin___emoji___add: { + 'admin/emoji/import-zip': { 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[]; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['EmojiDetailed']; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -7517,25 +6921,28 @@ export type operations = { }; }; /** - * admin/emoji/adds + * admin/emoji/list-remote * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ - admin___emoji___adds: { + 'admin/emoji/list-remote': { requestBody: { content: { 'application/json': { - name?: string; + /** @default null */ + query?: string | null; + /** + * @description Use `null` to represent the local host. + * @default null + */ + host?: string | null; + /** @default 10 */ + limit?: number; /** 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[]; + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; }; }; }; @@ -7543,7 +6950,16 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['EmojiDetailed']; + 'application/json': ({ + /** Format: id */ + id: string; + aliases: string[]; + name: string; + category: string | null; + /** @description The local host is represented with `null`. */ + host: string | null; + url: string; + })[]; }; }; /** @description Client error */ @@ -7579,17 +6995,23 @@ export type operations = { }; }; /** - * admin/emoji/copy + * admin/emoji/list * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ - admin___emoji___copy: { + 'admin/emoji/list': { requestBody: { content: { 'application/json': { + /** @default null */ + query?: string | null; + /** @default 10 */ + limit?: number; /** Format: misskey:id */ - emojiId: string; + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; }; }; }; @@ -7597,10 +7019,16 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - /** Format: id */ - id: string; - }; + 'application/json': ({ + /** Format: id */ + id: string; + 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. */ + host: string | null; + url: string; + })[]; }; }; /** @description Client error */ @@ -7636,16 +7064,17 @@ export type operations = { }; }; /** - * admin/emoji/delete-bulk + * admin/emoji/remove-aliases-bulk * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - 'admin___emoji___delete-bulk': { + 'admin/emoji/remove-aliases-bulk': { requestBody: { content: { 'application/json': { ids: string[]; + aliases: string[]; }; }; }; @@ -7687,17 +7116,17 @@ export type operations = { }; }; /** - * admin/emoji/delete + * admin/emoji/set-aliases-bulk * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - admin___emoji___delete: { + 'admin/emoji/set-aliases-bulk': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - id: string; + ids: string[]; + aliases: string[]; }; }; }; @@ -7739,18 +7168,18 @@ export type operations = { }; }; /** - * admin/emoji/import-zip + * admin/emoji/set-category-bulk * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* + * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - 'admin___emoji___import-zip': { + 'admin/emoji/set-category-bulk': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - fileId: string; + ids: string[]; + /** @description Use `null` to reset the category. */ + category?: string | null; }; }; }; @@ -7792,46 +7221,25 @@ export type operations = { }; }; /** - * admin/emoji/list-remote + * admin/emoji/set-license-bulk * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - 'admin___emoji___list-remote': { + 'admin/emoji/set-license-bulk': { 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; - /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; + ids: string[]; + /** @description Use `null` to reset the license. */ + license?: string | null; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': ({ - /** Format: id */ - id: string; - aliases: string[]; - name: string; - category: string | null; - /** @description The local host is represented with `null`. */ - host: string | null; - url: string; - })[]; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -7866,23 +7274,17 @@ export type operations = { }; }; /** - * admin/emoji/list + * admin/emoji/steal * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - admin___emoji___list: { + 'admin/emoji/steal': { requestBody: { content: { 'application/json': { - /** @default null */ - query?: string | null; - /** @default 10 */ - limit?: number; - /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; + name: string; + host: string; }; }; }; @@ -7890,16 +7292,10 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': ({ - /** Format: id */ - id: string; - 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. */ - host: string | null; - url: string; - })[]; + 'application/json': { + /** Format: id */ + id: string; + }; }; }; /** @description Client error */ @@ -7935,17 +7331,27 @@ export type operations = { }; }; /** - * admin/emoji/remove-aliases-bulk + * admin/emoji/update * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ - 'admin___emoji___remove-aliases-bulk': { + 'admin/emoji/update': { requestBody: { content: { 'application/json': { - ids: string[]; + /** Format: misskey:id */ + id: 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[]; }; }; }; @@ -7987,17 +7393,16 @@ export type operations = { }; }; /** - * admin/emoji/set-aliases-bulk + * admin/federation/delete-all-files * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ - 'admin___emoji___set-aliases-bulk': { + 'admin/federation/delete-all-files': { requestBody: { content: { 'application/json': { - ids: string[]; - aliases: string[]; + host: string; }; }; }; @@ -8039,18 +7444,16 @@ export type operations = { }; }; /** - * admin/emoji/set-category-bulk + * admin/federation/refresh-remote-instance-metadata * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ - 'admin___emoji___set-category-bulk': { + 'admin/federation/refresh-remote-instance-metadata': { requestBody: { content: { 'application/json': { - ids: string[]; - /** @description Use `null` to reset the category. */ - category?: string | null; + host: string; }; }; }; @@ -8092,18 +7495,16 @@ export type operations = { }; }; /** - * admin/emoji/set-license-bulk + * admin/federation/remove-all-following * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ - 'admin___emoji___set-license-bulk': { + 'admin/federation/remove-all-following': { requestBody: { content: { 'application/json': { - ids: string[]; - /** @description Use `null` to reset the license. */ - license?: string | null; + host: string; }; }; }; @@ -8145,29 +7546,24 @@ export type operations = { }; }; /** - * admin/emoji/steal + * admin/federation/update-instance * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ - admin___emoji___steal: { + 'admin/federation/update-instance': { requestBody: { content: { 'application/json': { - name: string; host: string; + isSuspended: boolean; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': { - /** Format: id */ - id: string; - }; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -8202,34 +7598,21 @@ export type operations = { }; }; /** - * admin/emoji/update + * admin/get-index-stats * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* + * **Credential required**: *Yes* / **Permission**: *read:admin:index-stats* */ - admin___emoji___update: { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - id?: 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[]; - }; - }; - }; + 'admin/get-index-stats': { responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + tablename: string; + indexname: string; + }[]; + }; }; /** @description Client error */ 400: { @@ -8264,23 +7647,18 @@ export type operations = { }; }; /** - * admin/federation/delete-all-files + * admin/get-table-stats * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:federation* + * **Credential required**: *Yes* / **Permission**: *read:admin:table-stats* */ - 'admin___federation___delete-all-files': { - requestBody: { - content: { - 'application/json': { - host: string; - }; - }; - }; + 'admin/get-table-stats': { responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': Record; + }; }; /** @description Client error */ 400: { @@ -8315,23 +7693,30 @@ export type operations = { }; }; /** - * admin/federation/refresh-remote-instance-metadata + * admin/get-user-ips * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:federation* + * **Credential required**: *Yes* / **Permission**: *read:admin:user-ips* */ - 'admin___federation___refresh-remote-instance-metadata': { + 'admin/get-user-ips': { requestBody: { content: { 'application/json': { - host: string; + /** Format: misskey:id */ + userId: string; }; }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + ip: string; + /** Format: date-time */ + createdAt: string; + }[]; + }; }; /** @description Client error */ 400: { @@ -8366,274 +7751,12 @@ export type operations = { }; }; /** - * admin/federation/remove-all-following + * admin/invite/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:federation* + * **Credential required**: *Yes* / **Permission**: *write:admin:invite-codes* */ - 'admin___federation___remove-all-following': { - requestBody: { - content: { - 'application/json': { - host: 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/federation/update-instance - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:federation* - */ - 'admin___federation___update-instance': { - requestBody: { - content: { - 'application/json': { - host: string; - isSuspended?: boolean; - moderationNote?: 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/get-index-stats - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *read:admin:index-stats* - */ - 'admin___get-index-stats': { - responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': { - tablename: string; - indexname: 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/get-table-stats - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *read:admin:table-stats* - */ - 'admin___get-table-stats': { - responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': { - [key: string]: { - count: number; - size: 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 Internal server error */ - 500: { - content: { - 'application/json': components['schemas']['Error']; - }; - }; - }; - }; - /** - * admin/get-user-ips - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *read:admin:user-ips* - */ - 'admin___get-user-ips': { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - userId: string; - }; - }; - }; - responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': { - ip: string; - /** Format: date-time */ - createdAt: 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/invite/create - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:invite-codes* - */ - admin___invite___create: { + 'admin/invite/create': { requestBody: { content: { 'application/json': { @@ -8688,7 +7811,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:invite-codes* */ - admin___invite___list: { + 'admin/invite/list': { requestBody: { content: { 'application/json': { @@ -8751,7 +7874,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:invite-codes* */ - admin___invite___revoke: { + 'admin/invite/revoke': { responses: { /** @description OK (without any results) */ 204: { @@ -8795,7 +7918,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:promo* */ - admin___promo___create: { + 'admin/promo/create': { requestBody: { content: { 'application/json': { @@ -8848,7 +7971,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ - admin___queue___clear: { + 'admin/queue/clear': { responses: { /** @description OK (without any results) */ 204: { @@ -8892,7 +8015,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:queue* */ - 'admin___queue___deliver-delayed': { + 'admin/queue/deliver-delayed': { responses: { /** @description OK (with results) */ 200: { @@ -8938,7 +8061,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:queue* */ - 'admin___queue___inbox-delayed': { + 'admin/queue/inbox-delayed': { responses: { /** @description OK (with results) */ 200: { @@ -8984,7 +8107,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ - admin___queue___promote: { + 'admin/queue/promote': { requestBody: { content: { 'application/json': { @@ -9036,7 +8159,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ - admin___queue___stats: { + 'admin/queue/stats': { responses: { /** @description OK (with results) */ 200: { @@ -9087,7 +8210,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:relays* */ - admin___relays___add: { + 'admin/relays/add': { requestBody: { content: { 'application/json': { @@ -9150,7 +8273,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:relays* */ - admin___relays___list: { + 'admin/relays/list': { responses: { /** @description OK (with results) */ 200: { @@ -9206,7 +8329,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:relays* */ - admin___relays___remove: { + 'admin/relays/remove': { requestBody: { content: { 'application/json': { @@ -9257,7 +8380,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:reset-password* */ - 'admin___reset-password': { + 'admin/reset-password': { requestBody: { content: { 'application/json': { @@ -9313,7 +8436,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report* */ - 'admin___resolve-abuse-user-report': { + 'admin/resolve-abuse-user-report': { requestBody: { content: { 'application/json': { @@ -9367,7 +8490,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:send-email* */ - 'admin___send-email': { + 'admin/send-email': { requestBody: { content: { 'application/json': { @@ -9420,7 +8543,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:server-info* */ - 'admin___server-info': { + 'admin/server-info': { responses: { /** @description OK (with results) */ 200: { @@ -9490,7 +8613,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log* */ - 'admin___show-moderation-logs': { + 'admin/show-moderation-logs': { requestBody: { content: { 'application/json': { @@ -9519,7 +8642,7 @@ export type operations = { info: Record; /** Format: id */ userId: string; - user: components['schemas']['UserDetailedNotMe']; + user: components['schemas']['UserDetailed']; }[]; }; }; @@ -9561,7 +8684,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:admin:show-user* */ - 'admin___show-user': { + 'admin/show-user': { requestBody: { content: { 'application/json': { @@ -9574,172 +8697,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - email: string | null; - emailVerified: boolean; - 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; - }]>; - groupInvited?: 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; - isSilenced: boolean; - isSuspended: boolean; - isSensitive: 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': Record; }; }; /** @description Client error */ @@ -9778,9 +8736,9 @@ export type operations = { * admin/show-users * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:show-user* + * **Credential required**: *Yes* / **Permission**: *read:admin:show-users* */ - 'admin___show-users': { + 'admin/show-users': { requestBody: { content: { 'application/json': { @@ -9855,7 +8813,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* */ - 'admin___suspend-user': { + 'admin/suspend-user': { requestBody: { content: { 'application/json': { @@ -9907,111 +8865,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user* */ - 'admin___unsuspend-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/set-user-sensitive - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* - */ - 'admin___set-user-sensitive': { - 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/unset-user-sensitive - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* - */ - 'admin___unset-user-sensitive': { + 'admin/unsuspend-user': { requestBody: { content: { 'application/json': { @@ -10063,7 +8917,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:admin:meta* */ - 'admin___update-meta': { + 'admin/update-meta': { requestBody: { content: { 'application/json': { @@ -10072,7 +8926,6 @@ export type operations = { hiddenTags?: string[] | null; blockedHosts?: string[] | null; sensitiveWords?: string[] | null; - prohibitedWords?: string[] | null; themeColor?: string | null; mascotImageUrl?: string | null; bannerUrl?: string | null; @@ -10095,10 +8948,6 @@ export type operations = { 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; @@ -10116,6 +8965,7 @@ export type operations = { maintainerName?: string | null; maintainerEmail?: string | null; langs?: string[]; + summalyProxy?: string | null; translatorType?: string | null; deeplAuthKey?: string | null; deeplIsPro?: boolean; @@ -10135,12 +8985,10 @@ export type operations = { swPublicKey?: string | null; swPrivateKey?: string | null; tosUrl?: string | null; - repositoryUrl?: string | null; - feedbackUrl?: string | null; + repositoryUrl?: string; + feedbackUrl?: string; impressumUrl?: string | null; privacyPolicyUrl?: string | null; - statusUrl?: string | null; - inquiryUrl?: string | null; useObjectStorage?: boolean; objectStorageBaseUrl?: string | null; objectStorageBucket?: string | null; @@ -10171,265 +9019,27 @@ export type operations = { enableActiveEmailValidation?: boolean; enableVerifymailApi?: boolean; verifymailAuthKey?: string | null; - enableTruemailApi?: boolean; - truemailInstance?: string | null; - truemailAuthKey?: string | null; enableChartsForRemoteUser?: boolean; - enableChartsForFederatedInstances?: boolean; - enableServerMachineStats?: boolean; - enableIdenticonGeneration?: boolean; - serverRules?: string[]; - bannedEmailDomains?: string[]; - preservedUsernames?: string[]; - manifestJsonOverride?: string; - enableFanoutTimeline?: boolean; - enableFanoutTimelineDbFallback?: boolean; - perLocalUserUserTimelineCacheMax?: number; - perRemoteUserUserTimelineCacheMax?: number; - perUserHomeTimelineCacheMax?: number; - perUserListTimelineCacheMax?: number; - 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; - urlPreviewDirectSummalyProxy?: boolean; - doNotSendNotificationEmailsForAbuseReport?: boolean; - emailToReceiveAbuseReport?: string | null; - enableReceivePrerelease?: boolean; - skipVersion?: boolean; - skipCherryPickVersion?: 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/delete-account - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account* - */ - '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/update-user-note - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:user-note* - */ - 'admin___update-user-note': { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - userId: 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']; - }; - }; - }; - }; - /** - * admin/roles/create - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:roles* - */ - admin___roles___create: { - requestBody: { - content: { - 'application/json': { - name: string; - description: string; - color: string | null; - iconUrl: string | null; - /** @enum {string} */ - target: 'manual' | 'conditional'; - condFormula: Record; - isPublic: boolean; - isModerator: boolean; - isAdministrator: boolean; - /** @default false */ - isExplorable?: boolean; - asBadge: boolean; - canEditMembersByModerator: boolean; - displayOrder: number; - policies: Record; - }; - }; - }; - responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['Role']; - }; - }; - /** @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/roles/delete - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:roles* - */ - admin___roles___delete: { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - roleId: string; + enableChartsForFederatedInstances?: boolean; + enableServerMachineStats?: boolean; + enableIdenticonGeneration?: boolean; + serverRules?: string[]; + bannedEmailDomains?: string[]; + preservedUsernames?: string[]; + manifestJsonOverride?: string; + enableFanoutTimeline?: boolean; + enableFanoutTimelineDbFallback?: boolean; + perLocalUserUserTimelineCacheMax?: number; + perRemoteUserUserTimelineCacheMax?: number; + perUserHomeTimelineCacheMax?: number; + perUserListTimelineCacheMax?: number; + notesPerOneAd?: number; + silencedHosts?: string[] | null; + doNotSendNotificationEmailsForAbuseReport?: boolean; + emailToReceiveAbuseReport?: string | null; + enableReceivePrerelease?: boolean; + skipVersion?: boolean; + skipCherryPickVersion?: string | null; }; }; }; @@ -10471,63 +9081,17 @@ export type operations = { }; }; /** - * admin/roles/list - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *read:admin:roles* - */ - admin___roles___list: { - responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['Role'][]; - }; - }; - /** @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/roles/show + * admin/delete-account * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:roles* + * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account* */ - admin___roles___show: { + 'admin/delete-account': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - roleId: string; + userId: string; }; }; }; @@ -10535,7 +9099,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['Role']; + 'application/json': unknown; }; }; /** @description Client error */ @@ -10571,32 +9135,18 @@ export type operations = { }; }; /** - * admin/roles/update + * admin/update-user-note * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:roles* + * **Credential required**: *Yes* / **Permission**: *write:admin:user-note* */ - admin___roles___update: { + 'admin/update-user-note': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - roleId: string; - name?: string; - description?: string; - color?: string | null; - iconUrl?: string | null; - /** @enum {string} */ - target?: 'manual' | 'conditional'; - condFormula?: Record; - isPublic?: boolean; - isModerator?: boolean; - isAdministrator?: boolean; - isExplorable?: boolean; - asBadge?: boolean; - canEditMembersByModerator?: boolean; - displayOrder?: number; - policies?: Record; + userId: string; + text: string; }; }; }; @@ -10638,82 +9188,41 @@ export type operations = { }; }; /** - * admin/roles/assign + * admin/roles/create * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - admin___roles___assign: { + 'admin/roles/create': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - roleId: string; - /** Format: misskey:id */ - userId: string; - expiresAt?: number | null; + name: string; + description: string; + color: string | null; + iconUrl: string | null; + /** @enum {string} */ + target: 'manual' | 'conditional'; + condFormula: Record; + isPublic: boolean; + isModerator: boolean; + isAdministrator: boolean; + /** @default false */ + isExplorable?: boolean; + asBadge: boolean; + canEditMembersByModerator: boolean; + displayOrder: number; + policies: Record; }; }; }; 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: { + /** @description OK (with results) */ + 200: { content: { - 'application/json': components['schemas']['Error']; - }; - }; - }; - }; - /** - * admin/roles/unassign - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:admin:roles* - */ - admin___roles___unassign: { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - roleId: string; - /** Format: misskey:id */ - userId: string; + 'application/json': components['schemas']['Role']; }; }; - }; - responses: { - /** @description OK (without any results) */ - 204: { - content: never; - }; /** @description Client error */ 400: { content: { @@ -10747,16 +9256,17 @@ export type operations = { }; }; /** - * admin/roles/update-default-policies + * admin/roles/delete * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - 'admin___roles___update-default-policies': { + 'admin/roles/delete': { requestBody: { content: { 'application/json': { - policies: Record; + /** Format: misskey:id */ + roleId: string; }; }; }; @@ -10798,39 +9308,17 @@ export type operations = { }; }; /** - * admin/roles/users + * admin/roles/list * @description No description provided. * - * **Credential required**: *No* / **Permission**: *read:admin:roles* + * **Credential required**: *Yes* / **Permission**: *read:admin:roles* */ - admin___roles___users: { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - roleId: string; - /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; - /** @default 10 */ - limit?: number; - }; - }; - }; + 'admin/roles/list': { 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; - })[]; + 'application/json': components['schemas']['Role'][]; }; }; /** @description Client error */ @@ -10866,21 +9354,17 @@ export type operations = { }; }; /** - * admin/system-webhook/create + * admin/roles/show * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* + * **Credential required**: *Yes* / **Permission**: *read:admin:roles* */ - 'admin___system-webhook___create': { + 'admin/roles/show': { requestBody: { content: { 'application/json': { - isActive: boolean; - name: string; - on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; - url: string; - secret: string; + /** Format: misskey:id */ + roleId: string; }; }; }; @@ -10888,7 +9372,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['SystemWebhook']; + 'application/json': components['schemas']['Role']; }; }; /** @description Client error */ @@ -10924,18 +9408,32 @@ export type operations = { }; }; /** - * admin/system-webhook/delete + * admin/roles/update * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* + * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - 'admin___system-webhook___delete': { + 'admin/roles/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - id: string; + roleId: string; + name: string; + description: string; + color: string | null; + iconUrl: string | null; + /** @enum {string} */ + target: 'manual' | 'conditional'; + condFormula: Record; + isPublic: boolean; + isModerator: boolean; + isAdministrator: boolean; + isExplorable?: boolean; + asBadge: boolean; + canEditMembersByModerator: boolean; + displayOrder: number; + policies: Record; }; }; }; @@ -10977,27 +9475,27 @@ export type operations = { }; }; /** - * admin/system-webhook/list + * admin/roles/assign * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* + * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - 'admin___system-webhook___list': { + 'admin/roles/assign': { requestBody: { content: { 'application/json': { - isActive?: boolean; - on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; + /** Format: misskey:id */ + roleId: string; + /** Format: misskey:id */ + userId: string; + expiresAt?: number | null; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['SystemWebhook'][]; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -11032,28 +9530,27 @@ export type operations = { }; }; /** - * admin/system-webhook/show + * admin/roles/unassign * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* + * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - 'admin___system-webhook___show': { + 'admin/roles/unassign': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - id: string; - }; - }; - }; - responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['SystemWebhook']; + roleId: string; + /** Format: misskey:id */ + userId: string; }; }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; /** @description Client error */ 400: { content: { @@ -11087,32 +9584,23 @@ export type operations = { }; }; /** - * admin/system-webhook/update + * admin/roles/update-default-policies * @description No description provided. * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* + * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - 'admin___system-webhook___update': { + 'admin/roles/update-default-policies': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - id: string; - isActive: boolean; - name: string; - on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; - url: string; - secret: string; + policies: Record; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['SystemWebhook']; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -11147,23 +9635,23 @@ export type operations = { }; }; /** - * announcements + * admin/roles/users * @description No description provided. * - * **Credential required**: *No* + * **Credential required**: *No* / **Permission**: *read:admin:roles* */ - announcements: { + 'admin/roles/users': { requestBody: { content: { 'application/json': { - /** @default 10 */ - limit?: number; + /** Format: misskey:id */ + roleId: string; /** Format: misskey:id */ sinceId?: string; /** Format: misskey:id */ untilId?: string; - /** @default true */ - isActive?: boolean; + /** @default 10 */ + limit?: number; }; }; }; @@ -11171,7 +9659,15 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['Announcement'][]; + '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 */ @@ -11207,17 +9703,23 @@ export type operations = { }; }; /** - * announcements/show + * announcements * @description No description provided. * * **Credential required**: *No* */ - announcements___show: { + announcements: { requestBody: { content: { 'application/json': { + /** @default 10 */ + limit?: number; /** Format: misskey:id */ - announcementId: string; + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default true */ + isActive?: boolean; }; }; }; @@ -11225,7 +9727,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['Announcement']; + 'application/json': components['schemas']['Announcement'][]; }; }; /** @description Client error */ @@ -11266,7 +9768,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - antennas___create: { + 'antennas/create': { requestBody: { content: { 'application/json': { @@ -11282,9 +9784,9 @@ export type operations = { users: string[]; caseSensitive: boolean; localOnly?: boolean; - excludeBots?: boolean; withReplies: boolean; withFile: boolean; + notify: boolean; }; }; }; @@ -11333,7 +9835,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - antennas___delete: { + 'antennas/delete': { requestBody: { content: { 'application/json': { @@ -11385,7 +9887,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - antennas___list: { + 'antennas/list': { responses: { /** @description OK (with results) */ 200: { @@ -11431,7 +9933,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - antennas___notes: { + 'antennas/notes': { requestBody: { content: { 'application/json': { @@ -11493,7 +9995,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - antennas___show: { + 'antennas/show': { requestBody: { content: { 'application/json': { @@ -11547,27 +10049,27 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - antennas___update: { + 'antennas/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ antennaId: string; - name?: string; + name: string; /** @enum {string} */ - src?: 'home' | 'all' | 'users' | 'list' | 'group' | 'users_blacklist'; + src: 'home' | 'all' | 'users' | 'list' | 'group' | 'users_blacklist'; /** Format: misskey:id */ userListId?: string | null; /** Format: misskey:id */ userGroupId?: string | null; - keywords?: string[][]; - excludeKeywords?: string[][]; - users?: string[]; - caseSensitive?: boolean; + keywords: string[][]; + excludeKeywords: string[][]; + users: string[]; + caseSensitive: boolean; localOnly?: boolean; - excludeBots?: boolean; - withReplies?: boolean; - withFile?: boolean; + withReplies: boolean; + withFile: boolean; + notify: boolean; }; }; }; @@ -11616,7 +10118,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:federation* */ - ap___get: { + 'ap/get': { requestBody: { content: { 'application/json': { @@ -11675,7 +10177,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - ap___show: { + 'ap/show': { requestBody: { content: { 'application/json': { @@ -11742,7 +10244,7 @@ export type operations = { * * **Credential required**: *No* */ - app___create: { + 'app/create': { requestBody: { content: { 'application/json': { @@ -11798,7 +10300,7 @@ export type operations = { * * **Credential required**: *No* */ - app___show: { + 'app/show': { requestBody: { content: { 'application/json': { @@ -11853,7 +10355,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - auth___accept: { + 'auth/accept': { requestBody: { content: { 'application/json': { @@ -11904,7 +10406,7 @@ export type operations = { * * **Credential required**: *No* */ - auth___session___generate: { + 'auth/session/generate': { requestBody: { content: { 'application/json': { @@ -11961,7 +10463,7 @@ export type operations = { * * **Credential required**: *No* */ - auth___session___show: { + 'auth/session/show': { requestBody: { content: { 'application/json': { @@ -12019,7 +10521,7 @@ export type operations = { * * **Credential required**: *No* */ - auth___session___userkey: { + 'auth/session/userkey': { requestBody: { content: { 'application/json': { @@ -12076,7 +10578,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:blocks* */ - blocking___create: { + 'blocking/create': { requestBody: { content: { 'application/json': { @@ -12136,7 +10638,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:blocks* */ - blocking___delete: { + 'blocking/delete': { requestBody: { content: { 'application/json': { @@ -12196,7 +10698,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:blocks* */ - blocking___list: { + 'blocking/list': { requestBody: { content: { 'application/json': { @@ -12254,7 +10756,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - channels___create: { + 'channels/create': { requestBody: { content: { 'application/json': { @@ -12319,7 +10821,7 @@ export type operations = { * * **Credential required**: *No* */ - channels___featured: { + 'channels/featured': { responses: { /** @description OK (with results) */ 200: { @@ -12365,7 +10867,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - channels___follow: { + 'channels/follow': { requestBody: { content: { 'application/json': { @@ -12417,7 +10919,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:channels* */ - channels___followed: { + 'channels/followed': { requestBody: { content: { 'application/json': { @@ -12475,7 +10977,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:channels* */ - channels___owned: { + 'channels/owned': { requestBody: { content: { 'application/json': { @@ -12533,7 +11035,7 @@ export type operations = { * * **Credential required**: *No* */ - channels___show: { + 'channels/show': { requestBody: { content: { 'application/json': { @@ -12587,7 +11089,7 @@ export type operations = { * * **Credential required**: *No* */ - channels___timeline: { + 'channels/timeline': { requestBody: { content: { 'application/json': { @@ -12651,7 +11153,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - channels___unfollow: { + 'channels/unfollow': { requestBody: { content: { 'application/json': { @@ -12703,7 +11205,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - channels___update: { + 'channels/update': { requestBody: { content: { 'application/json': { @@ -12766,7 +11268,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - channels___favorite: { + 'channels/favorite': { requestBody: { content: { 'application/json': { @@ -12818,7 +11320,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:channels* */ - channels___unfavorite: { + 'channels/unfavorite': { requestBody: { content: { 'application/json': { @@ -12870,7 +11372,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:channels* */ - 'channels___my-favorites': { + 'channels/my-favorites': { responses: { /** @description OK (with results) */ 200: { @@ -12916,7 +11418,7 @@ export type operations = { * * **Credential required**: *No* */ - channels___search: { + 'channels/search': { requestBody: { content: { 'application/json': { @@ -12980,7 +11482,7 @@ export type operations = { * * **Credential required**: *No* */ - 'charts___active-users': { + 'charts/active-users': { requestBody: { content: { 'application/json': { @@ -13048,7 +11550,7 @@ export type operations = { * * **Credential required**: *No* */ - 'charts___ap-request': { + 'charts/ap-request': { requestBody: { content: { 'application/json': { @@ -13110,7 +11612,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___drive: { + 'charts/drive': { requestBody: { content: { 'application/json': { @@ -13128,18 +11630,14 @@ export type operations = { 200: { content: { 'application/json': { - local: { - incCount: number[]; - incSize: number[]; - decCount: number[]; - decSize: number[]; - }; - remote: { - incCount: number[]; - incSize: number[]; - decCount: number[]; - decSize: number[]; - }; + 'local.incCount': number[]; + 'local.incSize': number[]; + 'local.decCount': number[]; + 'local.decSize': number[]; + 'remote.incCount': number[]; + 'remote.incSize': number[]; + 'remote.decCount': number[]; + 'remote.decSize': number[]; }; }; }; @@ -13181,7 +11679,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___federation: { + 'charts/federation': { requestBody: { content: { 'application/json': { @@ -13248,7 +11746,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___instance: { + 'charts/instance': { requestBody: { content: { 'application/json': { @@ -13267,44 +11765,30 @@ export type operations = { 200: { content: { 'application/json': { - requests: { - failed: number[]; - succeeded: number[]; - received: number[]; - }; - notes: { - total: number[]; - inc: number[]; - dec: number[]; - diffs: { - normal: number[]; - reply: number[]; - renote: number[]; - withFile: number[]; - }; - }; - users: { - total: number[]; - inc: number[]; - dec: number[]; - }; - following: { - total: number[]; - inc: number[]; - dec: number[]; - }; - followers: { - total: number[]; - inc: number[]; - dec: number[]; - }; - drive: { - totalFiles: number[]; - incFiles: number[]; - decFiles: number[]; - incUsage: number[]; - decUsage: number[]; - }; + 'requests.failed': number[]; + 'requests.succeeded': number[]; + 'requests.received': number[]; + 'notes.total': number[]; + 'notes.inc': number[]; + 'notes.dec': number[]; + 'notes.diffs.normal': number[]; + 'notes.diffs.reply': number[]; + 'notes.diffs.renote': number[]; + 'notes.diffs.withFile': number[]; + 'users.total': number[]; + 'users.inc': number[]; + 'users.dec': number[]; + 'following.total': number[]; + 'following.inc': number[]; + 'following.dec': number[]; + 'followers.total': number[]; + 'followers.inc': number[]; + 'followers.dec': number[]; + 'drive.totalFiles': number[]; + 'drive.incFiles': number[]; + 'drive.decFiles': number[]; + 'drive.incUsage': number[]; + 'drive.decUsage': number[]; }; }; }; @@ -13346,7 +11830,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___notes: { + 'charts/notes': { requestBody: { content: { 'application/json': { @@ -13364,28 +11848,20 @@ export type operations = { 200: { content: { 'application/json': { - local: { - total: number[]; - inc: number[]; - dec: number[]; - diffs: { - normal: number[]; - reply: number[]; - renote: number[]; - withFile: number[]; - }; - }; - remote: { - total: number[]; - inc: number[]; - dec: number[]; - diffs: { - normal: number[]; - reply: number[]; - renote: number[]; - withFile: number[]; - }; - }; + 'local.total': number[]; + 'local.inc': number[]; + 'local.dec': number[]; + 'local.diffs.normal': number[]; + 'local.diffs.reply': number[]; + 'local.diffs.renote': number[]; + 'local.diffs.withFile': number[]; + 'remote.total': number[]; + 'remote.inc': number[]; + 'remote.dec': number[]; + 'remote.diffs.normal': number[]; + 'remote.diffs.reply': number[]; + 'remote.diffs.renote': number[]; + 'remote.diffs.withFile': number[]; }; }; }; @@ -13427,7 +11903,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___user___drive: { + 'charts/user/drive': { requestBody: { content: { 'application/json': { @@ -13494,7 +11970,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___user___following: { + 'charts/user/following': { requestBody: { content: { 'application/json': { @@ -13514,30 +11990,18 @@ export type operations = { 200: { content: { 'application/json': { - local: { - followings: { - total: number[]; - inc: number[]; - dec: number[]; - }; - followers: { - total: number[]; - inc: number[]; - dec: number[]; - }; - }; - remote: { - followings: { - total: number[]; - inc: number[]; - dec: number[]; - }; - followers: { - total: number[]; - inc: number[]; - dec: number[]; - }; - }; + 'local.followings.total': number[]; + 'local.followings.inc': number[]; + 'local.followings.dec': number[]; + 'local.followers.total': number[]; + 'local.followers.inc': number[]; + 'local.followers.dec': number[]; + 'remote.followings.total': number[]; + 'remote.followings.inc': number[]; + 'remote.followings.dec': number[]; + 'remote.followers.total': number[]; + 'remote.followers.inc': number[]; + 'remote.followers.dec': number[]; }; }; }; @@ -13579,7 +12043,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___user___notes: { + 'charts/user/notes': { requestBody: { content: { 'application/json': { @@ -13602,12 +12066,10 @@ export type operations = { total: number[]; inc: number[]; dec: number[]; - diffs: { - normal: number[]; - reply: number[]; - renote: number[]; - withFile: number[]; - }; + 'diffs.normal': number[]; + 'diffs.reply': number[]; + 'diffs.renote': number[]; + 'diffs.withFile': number[]; }; }; }; @@ -13649,7 +12111,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___user___pv: { + 'charts/user/pv': { requestBody: { content: { 'application/json': { @@ -13669,14 +12131,10 @@ export type operations = { 200: { content: { 'application/json': { - upv: { - user: number[]; - visitor: number[]; - }; - pv: { - user: number[]; - visitor: number[]; - }; + 'upv.user': number[]; + 'pv.user': number[]; + 'upv.visitor': number[]; + 'pv.visitor': number[]; }; }; }; @@ -13718,7 +12176,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___user___reactions: { + 'charts/user/reactions': { requestBody: { content: { 'application/json': { @@ -13738,12 +12196,8 @@ export type operations = { 200: { content: { 'application/json': { - local: { - count: number[]; - }; - remote: { - count: number[]; - }; + 'local.count': number[]; + 'remote.count': number[]; }; }; }; @@ -13785,7 +12239,7 @@ export type operations = { * * **Credential required**: *No* */ - charts___users: { + 'charts/users': { requestBody: { content: { 'application/json': { @@ -13803,16 +12257,12 @@ export type operations = { 200: { content: { 'application/json': { - local: { - total: number[]; - inc: number[]; - dec: number[]; - }; - remote: { - total: number[]; - inc: number[]; - dec: number[]; - }; + 'local.total': number[]; + 'local.inc': number[]; + 'local.dec': number[]; + 'remote.total': number[]; + 'remote.inc': number[]; + 'remote.dec': number[]; }; }; }; @@ -13854,7 +12304,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'clips___add-note': { + 'clips/add-note': { requestBody: { content: { 'application/json': { @@ -13914,7 +12364,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'clips___remove-note': { + 'clips/remove-note': { requestBody: { content: { 'application/json': { @@ -13968,7 +12418,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - clips___create: { + 'clips/create': { requestBody: { content: { 'application/json': { @@ -14024,7 +12474,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - clips___delete: { + 'clips/delete': { requestBody: { content: { 'application/json': { @@ -14076,7 +12526,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - clips___list: { + 'clips/list': { responses: { /** @description OK (with results) */ 200: { @@ -14122,7 +12572,7 @@ export type operations = { * * **Credential required**: *No* / **Permission**: *read:account* */ - clips___notes: { + 'clips/notes': { requestBody: { content: { 'application/json': { @@ -14182,7 +12632,7 @@ export type operations = { * * **Credential required**: *No* / **Permission**: *read:account* */ - clips___show: { + 'clips/show': { requestBody: { content: { 'application/json': { @@ -14236,13 +12686,13 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - clips___update: { + 'clips/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ clipId: string; - name?: string; + name: string; isPublic?: boolean; description?: string | null; }; @@ -14293,7 +12743,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* */ - clips___favorite: { + 'clips/favorite': { requestBody: { content: { 'application/json': { @@ -14345,7 +12795,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* */ - clips___unfavorite: { + 'clips/unfavorite': { requestBody: { content: { 'application/json': { @@ -14397,7 +12847,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:clip-favorite* */ - 'clips___my-favorites': { + 'clips/my-favorites': { responses: { /** @description OK (with results) */ 200: { @@ -14492,7 +12942,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - drive___files: { + 'drive/files': { requestBody: { content: { 'application/json': { @@ -14558,7 +13008,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - 'drive___files___attached-notes': { + 'drive/files/attached-notes': { requestBody: { content: { 'application/json': { @@ -14618,7 +13068,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - 'drive___files___check-existence': { + 'drive/files/check-existence': { requestBody: { content: { 'application/json': { @@ -14671,7 +13121,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - drive___files___create: { + 'drive/files/create': { requestBody: { content: { 'multipart/form-data': { @@ -14692,7 +13142,7 @@ export type operations = { * Format: binary * @description The file contents. */ - file: Blob; + file: string; }; }; }; @@ -14747,7 +13197,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - drive___files___delete: { + 'drive/files/delete': { requestBody: { content: { 'application/json': { @@ -14799,7 +13249,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - 'drive___files___find-by-hash': { + 'drive/files/find-by-hash': { requestBody: { content: { 'application/json': { @@ -14852,7 +13302,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - drive___files___find: { + 'drive/files/find': { requestBody: { content: { 'application/json': { @@ -14910,7 +13360,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - drive___files___show: { + 'drive/files/show': { requestBody: { content: { 'application/json': { @@ -14965,7 +13415,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - drive___files___update: { + 'drive/files/update': { requestBody: { content: { 'application/json': { @@ -15024,7 +13474,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - 'drive___files___upload-from-url': { + 'drive/files/upload-from-url': { requestBody: { content: { 'application/json': { @@ -15094,7 +13544,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - drive___folders: { + 'drive/folders': { requestBody: { content: { 'application/json': { @@ -15157,7 +13607,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - drive___folders___create: { + 'drive/folders/create': { requestBody: { content: { 'application/json': { @@ -15219,7 +13669,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - drive___folders___delete: { + 'drive/folders/delete': { requestBody: { content: { 'application/json': { @@ -15271,7 +13721,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - drive___folders___find: { + 'drive/folders/find': { requestBody: { content: { 'application/json': { @@ -15329,7 +13779,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - drive___folders___show: { + 'drive/folders/show': { requestBody: { content: { 'application/json': { @@ -15383,7 +13833,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:drive* */ - drive___folders___update: { + 'drive/folders/update': { requestBody: { content: { 'application/json': { @@ -15440,7 +13890,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:drive* */ - drive___stream: { + 'drive/stream': { requestBody: { content: { 'application/json': { @@ -15499,7 +13949,7 @@ export type operations = { * * **Credential required**: *No* */ - 'email-address___available': { + 'email-address/available': { requestBody: { content: { 'application/json': { @@ -15714,7 +14164,7 @@ export type operations = { * * **Credential required**: *No* */ - federation___followers: { + 'federation/followers': { requestBody: { content: { 'application/json': { @@ -15773,7 +14223,7 @@ export type operations = { * * **Credential required**: *No* */ - federation___following: { + 'federation/following': { requestBody: { content: { 'application/json': { @@ -15832,7 +14282,7 @@ export type operations = { * * **Credential required**: *No* */ - federation___instances: { + 'federation/instances': { requestBody: { content: { 'application/json': { @@ -15899,7 +14349,7 @@ export type operations = { * * **Credential required**: *No* */ - 'federation___show-instance': { + 'federation/show-instance': { requestBody: { content: { 'application/json': { @@ -15956,7 +14406,7 @@ export type operations = { * * **Credential required**: *No* */ - 'federation___update-remote-user': { + 'federation/update-remote-user': { requestBody: { content: { 'application/json': { @@ -16008,7 +14458,7 @@ export type operations = { * * **Credential required**: *No* */ - federation___users: { + 'federation/users': { requestBody: { content: { 'application/json': { @@ -16067,7 +14517,7 @@ export type operations = { * * **Credential required**: *No* */ - federation___stats: { + 'federation/stats': { requestBody: { content: { 'application/json': { @@ -16126,7 +14576,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - following___create: { + 'following/create': { requestBody: { content: { 'application/json': { @@ -16187,7 +14637,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - following___delete: { + 'following/delete': { requestBody: { content: { 'application/json': { @@ -16247,7 +14697,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - following___update: { + 'following/update': { requestBody: { content: { 'application/json': { @@ -16310,7 +14760,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - 'following___update-all': { + 'following/update-all': { requestBody: { content: { 'application/json': { @@ -16369,7 +14819,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - following___invalidate: { + 'following/invalidate': { requestBody: { content: { 'application/json': { @@ -16429,7 +14879,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - following___requests___accept: { + 'following/requests/accept': { requestBody: { content: { 'application/json': { @@ -16481,7 +14931,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - following___requests___cancel: { + 'following/requests/cancel': { requestBody: { content: { 'application/json': { @@ -16535,7 +14985,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:following* */ - following___requests___list: { + 'following/requests/list': { requestBody: { content: { 'application/json': { @@ -16598,7 +15048,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:following* */ - following___requests___reject: { + 'following/requests/reject': { requestBody: { content: { 'application/json': { @@ -16650,7 +15100,7 @@ export type operations = { * * **Credential required**: *No* */ - gallery___featured: { + 'gallery/featured': { requestBody: { content: { 'application/json': { @@ -16706,7 +15156,7 @@ export type operations = { * * **Credential required**: *No* */ - gallery___popular: { + 'gallery/popular': { responses: { /** @description OK (with results) */ 200: { @@ -16752,7 +15202,7 @@ export type operations = { * * **Credential required**: *No* */ - gallery___posts: { + 'gallery/posts': { requestBody: { content: { 'application/json': { @@ -16810,7 +15260,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ - gallery___posts___create: { + 'gallery/posts/create': { requestBody: { content: { 'application/json': { @@ -16873,7 +15323,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ - gallery___posts___delete: { + 'gallery/posts/delete': { requestBody: { content: { 'application/json': { @@ -16925,7 +15375,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* */ - gallery___posts___like: { + 'gallery/posts/like': { requestBody: { content: { 'application/json': { @@ -16977,7 +15427,7 @@ export type operations = { * * **Credential required**: *No* */ - gallery___posts___show: { + 'gallery/posts/show': { requestBody: { content: { 'application/json': { @@ -17031,7 +15481,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* */ - gallery___posts___unlike: { + 'gallery/posts/unlike': { requestBody: { content: { 'application/json': { @@ -17083,15 +15533,15 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ - gallery___posts___update: { + 'gallery/posts/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ postId: string; - title?: string; + title: string; description?: string | null; - fileIds?: string[]; + fileIds: string[]; /** @default false */ isSensitive?: boolean; }; @@ -17252,7 +15702,7 @@ export type operations = { * * **Credential required**: *No* */ - hashtags___list: { + 'hashtags/list': { requestBody: { content: { 'application/json': { @@ -17314,7 +15764,7 @@ export type operations = { * * **Credential required**: *No* */ - hashtags___search: { + 'hashtags/search': { requestBody: { content: { 'application/json': { @@ -17371,7 +15821,7 @@ export type operations = { * * **Credential required**: *No* */ - hashtags___show: { + 'hashtags/show': { requestBody: { content: { 'application/json': { @@ -17424,7 +15874,7 @@ export type operations = { * * **Credential required**: *No* */ - hashtags___trend: { + 'hashtags/trend': { responses: { /** @description OK (with results) */ 200: { @@ -17474,7 +15924,7 @@ export type operations = { * * **Credential required**: *No* */ - hashtags___users: { + 'hashtags/users': { requestBody: { content: { 'application/json': { @@ -17588,7 +16038,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - i___2fa___done: { + 'i/2fa/done': { requestBody: { content: { 'application/json': { @@ -17597,13 +16047,9 @@ export type operations = { }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': { - backupCodes: string[]; - }; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -17644,7 +16090,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___2fa___key-done': { + 'i/2fa/key-done': { requestBody: { content: { 'application/json': { @@ -17704,7 +16150,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___2fa___password-less': { + 'i/2fa/password-less': { requestBody: { content: { 'application/json': { @@ -17756,7 +16202,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___2fa___register-key': { + 'i/2fa/register-key': { requestBody: { content: { 'application/json': { @@ -17771,7 +16217,7 @@ export type operations = { content: { 'application/json': { rp: { - id?: string; + id: string | null; }; user: { id: string; @@ -17845,7 +16291,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - i___2fa___register: { + 'i/2fa/register': { requestBody: { content: { 'application/json': { @@ -17906,7 +16352,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___2fa___update-key': { + 'i/2fa/update-key': { requestBody: { content: { 'application/json': { @@ -17959,7 +16405,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___2fa___remove-key': { + 'i/2fa/remove-key': { requestBody: { content: { 'application/json': { @@ -18013,7 +16459,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - i___2fa___unregister: { + 'i/2fa/unregister': { requestBody: { content: { 'application/json': { @@ -18066,7 +16512,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - i___apps: { + 'i/apps': { requestBody: { content: { 'application/json': { @@ -18082,11 +16528,11 @@ export type operations = { 'application/json': { /** Format: misskey:id */ id: string; - name?: string; + name: string; /** Format: date-time */ createdAt: string; /** Format: date-time */ - lastUsedAt?: string; + lastUsedAt: string; permission: string[]; }[]; }; @@ -18130,7 +16576,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___authorized-apps': { + 'i/authorized-apps': { requestBody: { content: { 'application/json': { @@ -18156,7 +16602,7 @@ export type operations = { name: string; callbackUrl: string | null; permission: string[]; - isAuthorized?: boolean; + isAuthorized: boolean; })[]; }; }; @@ -18198,12 +16644,12 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'i___claim-achievement': { + 'i/claim-achievement': { 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' | 'iLoveCherryPick' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'setNameToNoriDev' | 'setNameToYojo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead'; + 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' | 'iLoveCherryPick' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'setNameToNoriDev' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted'; }; }; }; @@ -18251,7 +16697,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___change-password': { + 'i/change-password': { requestBody: { content: { 'application/json': { @@ -18305,7 +16751,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___delete-account': { + 'i/delete-account': { requestBody: { content: { 'application/json': { @@ -18358,7 +16804,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___export-blocking': { + 'i/export-blocking': { responses: { /** @description OK (without any results) */ 204: { @@ -18409,7 +16855,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___export-following': { + 'i/export-following': { requestBody: { content: { 'application/json': { @@ -18470,7 +16916,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___export-mute': { + 'i/export-mute': { responses: { /** @description OK (without any results) */ 204: { @@ -18521,58 +16967,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___export-notes': { - 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 To many requests */ - 429: { - content: { - 'application/json': components['schemas']['Error']; - }; - }; - /** @description Internal server error */ - 500: { - content: { - 'application/json': components['schemas']['Error']; - }; - }; - }; - }; - /** - * i/export-clips - * @description No description provided. - * - * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. - * **Credential required**: *Yes* - */ - 'i___export-clips': { + 'i/export-notes': { responses: { /** @description OK (without any results) */ 204: { @@ -18623,7 +17018,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___export-favorites': { + 'i/export-favorites': { responses: { /** @description OK (without any results) */ 204: { @@ -18674,7 +17069,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___export-user-lists': { + 'i/export-user-lists': { responses: { /** @description OK (without any results) */ 204: { @@ -18725,7 +17120,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___export-antennas': { + 'i/export-antennas': { responses: { /** @description OK (without any results) */ 204: { @@ -18775,7 +17170,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:favorites* */ - i___favorites: { + 'i/favorites': { requestBody: { content: { 'application/json': { @@ -18833,7 +17228,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:gallery-likes* */ - i___gallery___likes: { + 'i/gallery/likes': { requestBody: { content: { 'application/json': { @@ -18895,7 +17290,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:gallery* */ - i___gallery___posts: { + 'i/gallery/posts': { requestBody: { content: { 'application/json': { @@ -18954,7 +17349,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___import-blocking': { + 'i/import-blocking': { requestBody: { content: { 'application/json': { @@ -19013,7 +17408,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___import-following': { + 'i/import-following': { requestBody: { content: { 'application/json': { @@ -19073,7 +17468,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___import-muting': { + 'i/import-muting': { requestBody: { content: { 'application/json': { @@ -19132,7 +17527,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___import-user-lists': { + 'i/import-user-lists': { requestBody: { content: { 'application/json': { @@ -19191,7 +17586,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___import-antennas': { + 'i/import-antennas': { requestBody: { content: { 'application/json': { @@ -19249,7 +17644,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:notifications* */ - i___notifications: { + 'i/notifications': { requestBody: { content: { 'application/json': { @@ -19261,8 +17656,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'groupInvited' | 'achievementEarned' | 'app' | 'test' | 'pollVote')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'groupInvited' | 'achievementEarned' | 'app' | 'test' | 'pollVote')[]; }; }; }; @@ -19317,7 +17712,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:notifications* */ - 'i___notifications-grouped': { + 'i/notifications-grouped': { requestBody: { content: { 'application/json': { @@ -19329,8 +17724,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'groupInvited' | 'achievementEarned' | 'app' | 'test' | 'pollVote')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'groupInvited' | 'achievementEarned' | 'app' | 'test' | 'pollVote')[]; }; }; }; @@ -19385,7 +17780,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:page-likes* */ - 'i___page-likes': { + 'i/page-likes': { requestBody: { content: { 'application/json': { @@ -19447,7 +17842,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:pages* */ - i___pages: { + 'i/pages': { requestBody: { content: { 'application/json': { @@ -19505,7 +17900,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - i___pin: { + 'i/pin': { requestBody: { content: { 'application/json': { @@ -19559,7 +17954,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'i___read-all-messaging-messages': { + 'i/read-all-messaging-messages': { responses: { /** @description OK (without any results) */ 204: { @@ -19603,7 +17998,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'i___read-all-unread-notes': { + 'i/read-all-unread-notes': { responses: { /** @description OK (without any results) */ 204: { @@ -19647,7 +18042,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'i___read-announcement': { + 'i/read-announcement': { requestBody: { content: { 'application/json': { @@ -19700,7 +18095,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___regenerate-token': { + 'i/regenerate-token': { requestBody: { content: { 'application/json': { @@ -19751,7 +18146,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - 'i___registry___get-all': { + 'i/registry/get-all': { requestBody: { content: { 'application/json': { @@ -19806,7 +18201,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - 'i___registry___get-detail': { + 'i/registry/get-detail': { requestBody: { content: { 'application/json': { @@ -19821,10 +18216,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - updatedAt: string; - value: unknown; - }; + 'application/json': Record; }; }; /** @description Client error */ @@ -19865,7 +18257,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - i___registry___get: { + 'i/registry/get': { requestBody: { content: { 'application/json': { @@ -19921,7 +18313,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - 'i___registry___keys-with-type': { + 'i/registry/keys-with-type': { requestBody: { content: { 'application/json': { @@ -19935,9 +18327,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - [key: string]: string; - }; + 'application/json': Record; }; }; /** @description Client error */ @@ -19978,7 +18368,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - i___registry___keys: { + 'i/registry/keys': { requestBody: { content: { 'application/json': { @@ -19989,11 +18379,9 @@ export type operations = { }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': string[]; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -20033,7 +18421,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - i___registry___remove: { + 'i/registry/remove': { requestBody: { content: { 'application/json': { @@ -20088,7 +18476,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___registry___scopes-with-domain': { + 'i/registry/scopes-with-domain': { responses: { /** @description OK (with results) */ 200: { @@ -20137,7 +18525,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - i___registry___set: { + 'i/registry/set': { requestBody: { content: { 'application/json': { @@ -20193,7 +18581,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___revoke-token': { + 'i/revoke-token': { requestBody: { content: { 'application/json': { @@ -20247,7 +18635,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___signin-history': { + 'i/signin-history': { requestBody: { content: { 'application/json': { @@ -20305,7 +18693,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - i___unpin: { + 'i/unpin': { requestBody: { content: { 'application/json': { @@ -20360,7 +18748,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'i___update-email': { + 'i/update-email': { requestBody: { content: { 'application/json': { @@ -20374,7 +18762,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['MeDetailed']; + 'application/json': components['schemas']['UserDetailed']; }; }; /** @description Client error */ @@ -20421,7 +18809,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - i___update: { + 'i/update': { requestBody: { content: { 'application/json': { @@ -20457,7 +18845,6 @@ export type operations = { autoAcceptFollowed?: boolean; noCrawle?: boolean; preventAiLearning?: boolean; - isIndexable?: boolean; isBot?: boolean; isCat?: boolean; injectFeaturedNote?: boolean; @@ -20473,143 +18860,7 @@ export type operations = { mutedWords?: (string[] | string)[]; hardMutedWords?: (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; - }]>; - groupInvited?: 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; - }]>; - }; + notificationRecieveConfig?: Record; emailNotificationTypes?: string[]; alsoKnownAs?: string[]; }; @@ -20666,7 +18917,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:user-groups* */ - 'i___user-group-invites': { + 'i/user-group-invites': { requestBody: { content: { 'application/json': { @@ -20729,7 +18980,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - i___move: { + 'i/move': { requestBody: { content: { 'application/json': { @@ -20788,7 +19039,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - i___webhooks___create: { + 'i/webhooks/create': { requestBody: { content: { 'application/json': { @@ -20858,7 +19109,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - i___webhooks___list: { + 'i/webhooks/list': { responses: { /** @description OK (with results) */ 200: { @@ -20917,7 +19168,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - i___webhooks___show: { + 'i/webhooks/show': { requestBody: { content: { 'application/json': { @@ -20984,17 +19235,18 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - i___webhooks___update: { + 'i/webhooks/update': { 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')[]; - active?: boolean; + name: string; + url: string; + /** @default */ + secret?: string; + on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + active: boolean; }; }; }; @@ -21041,7 +19293,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - i___webhooks___delete: { + 'i/webhooks/delete': { requestBody: { content: { 'application/json': { @@ -21093,7 +19345,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:invite-codes* */ - invite___create: { + 'invite/create': { responses: { /** @description OK (with results) */ 200: { @@ -21139,7 +19391,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:invite-codes* */ - invite___delete: { + 'invite/delete': { requestBody: { content: { 'application/json': { @@ -21191,7 +19443,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:invite-codes* */ - invite___list: { + 'invite/list': { requestBody: { content: { 'application/json': { @@ -21249,7 +19501,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:invite-codes* */ - invite___limit: { + 'invite/limit': { responses: { /** @description OK (with results) */ 200: { @@ -21297,7 +19549,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:messaging* */ - messaging___history: { + 'messaging/history': { requestBody: { content: { 'application/json': { @@ -21353,7 +19605,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:messaging* */ - messaging___messages: { + 'messaging/messages': { requestBody: { content: { 'application/json': { @@ -21417,7 +19669,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:messaging* */ - messaging___messages___create: { + 'messaging/messages/create': { requestBody: { content: { 'application/json': { @@ -21482,7 +19734,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:messaging* */ - messaging___messages___delete: { + 'messaging/messages/delete': { requestBody: { content: { 'application/json': { @@ -21540,7 +19792,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:messaging* */ - messaging___messages___read: { + 'messaging/messages/read': { requestBody: { content: { 'application/json': { @@ -21605,7 +19857,87 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['MetaLite'] | components['schemas']['MetaDetailed']; + 'application/json': { + maintainerName: string | null; + maintainerEmail: string | null; + version: string; + basedMisskeyVersion: string; + name: string; + shortName: string | null; + /** + * Format: url + * @example https://cherrypick.example.com + */ + uri: string; + description: string | null; + langs: string[]; + tosUrl: string | null; + /** @default https://github.com/kokonect-link/cherrypick */ + repositoryUrl: string; + /** @default https://github.com/kokonect-link/cherrypick/issues/new */ + feedbackUrl: string; + defaultDarkTheme: string | null; + defaultLightTheme: string | null; + disableRegistration: boolean; + cacheRemoteFiles: boolean; + cacheRemoteSensitiveFiles: boolean; + emailRequiredForSignup: boolean; + enableHcaptcha: boolean; + hcaptchaSiteKey: string | null; + enableRecaptcha: boolean; + recaptchaSiteKey: string | null; + enableTurnstile: boolean; + turnstileSiteKey: string | null; + swPublickey: string | null; + /** @default /assets/ai.png */ + mascotImageUrl: string; + bannerUrl: string; + serverErrorImageUrl: string | null; + infoImageUrl: string | null; + notFoundImageUrl: string | null; + iconUrl: string | null; + maxNoteTextLength: number; + ads: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: url */ + url: string; + place: string; + ratio: number; + /** Format: url */ + imageUrl: string; + dayOfWeek: number; + }[]; + /** @default 0 */ + notesPerOneAd: number; + /** @example false */ + requireSetup: boolean; + enableEmail: boolean; + enableServiceWorker: boolean; + translatorAvailable: boolean; + proxyAccountName: string | null; + mediaProxy: string; + features?: { + registration: boolean; + localTimeline: boolean; + globalTimeline: boolean; + hcaptcha: boolean; + recaptcha: boolean; + objectStorage: boolean; + serviceWorker: boolean; + /** @default true */ + miauth?: boolean; + }; + backgroundImageUrl: string | null; + impressumUrl: string | null; + logoImageUrl: string | null; + privacyPolicyUrl: string | null; + serverRules: string[]; + themeColor: string | null; + }; }; }; /** @description Client error */ @@ -21748,7 +20080,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'miauth___gen-token': { + 'miauth/gen-token': { requestBody: { content: { 'application/json': { @@ -21807,7 +20139,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ - mute___create: { + 'mute/create': { requestBody: { content: { 'application/json': { @@ -21867,7 +20199,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ - mute___delete: { + 'mute/delete': { requestBody: { content: { 'application/json': { @@ -21919,7 +20251,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:mutes* */ - mute___list: { + 'mute/list': { requestBody: { content: { 'application/json': { @@ -21977,7 +20309,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ - 'renote-mute___create': { + 'renote-mute/create': { requestBody: { content: { 'application/json': { @@ -22035,7 +20367,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ - 'renote-mute___delete': { + 'renote-mute/delete': { requestBody: { content: { 'application/json': { @@ -22087,7 +20419,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:mutes* */ - 'renote-mute___list': { + 'renote-mute/list': { requestBody: { content: { 'application/json': { @@ -22145,7 +20477,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - my___apps: { + 'my/apps': { requestBody: { content: { 'application/json': { @@ -22266,7 +20598,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___children: { + 'notes/children': { requestBody: { content: { 'application/json': { @@ -22326,7 +20658,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___clips: { + 'notes/clips': { requestBody: { content: { 'application/json': { @@ -22380,7 +20712,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___conversation: { + 'notes/conversation': { requestBody: { content: { 'application/json': { @@ -22438,7 +20770,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - notes___create: { + 'notes/create': { requestBody: { content: { 'application/json': { @@ -22446,7 +20778,7 @@ export type operations = { * @default public * @enum {string} */ - visibility?: 'public' | 'home' | 'followers' | 'specified' | 'private'; + visibility?: 'public' | 'home' | 'followers' | 'specified'; visibleUserIds?: string[]; cw?: string | null; /** @default false */ @@ -22485,10 +20817,6 @@ export type operations = { end?: number | null; metadata?: Record; }) | null; - scheduledDelete?: ({ - deleteAt?: number | null; - deleteAfter?: number | null; - }) | null; }; }; }; @@ -22545,7 +20873,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - notes___delete: { + 'notes/delete': { requestBody: { content: { 'application/json': { @@ -22603,7 +20931,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - notes___update: { + 'notes/update': { requestBody: { content: { 'application/json': { @@ -22673,7 +21001,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:favorites* */ - notes___favorites___create: { + 'notes/favorites/create': { requestBody: { content: { 'application/json': { @@ -22731,7 +21059,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:favorites* */ - notes___favorites___delete: { + 'notes/favorites/delete': { requestBody: { content: { 'application/json': { @@ -22783,7 +21111,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___featured: { + 'notes/featured': { requestBody: { content: { 'application/json': { @@ -22841,7 +21169,7 @@ export type operations = { * * **Credential required**: *No* */ - 'notes___global-timeline': { + 'notes/global-timeline': { requestBody: { content: { 'application/json': { @@ -22851,8 +21179,6 @@ export type operations = { withRenotes?: boolean; /** @default false */ withCats?: boolean; - /** @default false */ - withoutBots?: boolean; /** @default 10 */ limit?: number; /** Format: misskey:id */ @@ -22909,7 +21235,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - 'notes___hybrid-timeline': { + 'notes/hybrid-timeline': { requestBody: { content: { 'application/json': { @@ -22937,8 +21263,6 @@ export type operations = { withReplies?: boolean; /** @default false */ withCats?: boolean; - /** @default false */ - withoutBots?: boolean; }; }; }; @@ -22987,7 +21311,7 @@ export type operations = { * * **Credential required**: *No* */ - 'notes___local-timeline': { + 'notes/local-timeline': { requestBody: { content: { 'application/json': { @@ -22999,8 +21323,6 @@ export type operations = { withReplies?: boolean; /** @default false */ withCats?: boolean; - /** @default false */ - withoutBots?: boolean; /** @default 10 */ limit?: number; /** Format: misskey:id */ @@ -23059,7 +21381,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - notes___mentions: { + 'notes/mentions': { requestBody: { content: { 'application/json': { @@ -23120,7 +21442,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - notes___polls___recommendation: { + 'notes/polls/recommendation': { requestBody: { content: { 'application/json': { @@ -23128,8 +21450,6 @@ export type operations = { limit?: number; /** @default 0 */ offset?: number; - /** @default false */ - excludeChannels?: boolean; }; }; }; @@ -23178,7 +21498,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:votes* */ - notes___polls___vote: { + 'notes/polls/vote': { requestBody: { content: { 'application/json': { @@ -23231,7 +21551,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___events___search: { + 'notes/events/search': { requestBody: { content: { 'application/json': { @@ -23263,7 +21583,7 @@ export type operations = { * @default startDate * @enum {string|null} */ - sortBy?: 'startDate' | 'createdAt'; + sortBy?: 'startDate' | 'createdAt' | null; }; }; }; @@ -23312,7 +21632,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___reactions: { + 'notes/reactions': { requestBody: { content: { 'application/json': { @@ -23373,7 +21693,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:reactions* */ - notes___reactions___create: { + 'notes/reactions/create': { requestBody: { content: { 'application/json': { @@ -23426,7 +21746,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:reactions* */ - notes___reactions___delete: { + 'notes/reactions/delete': { requestBody: { content: { 'application/json': { @@ -23484,7 +21804,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___renotes: { + 'notes/renotes': { requestBody: { content: { 'application/json': { @@ -23544,7 +21864,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___replies: { + 'notes/replies': { requestBody: { content: { 'application/json': { @@ -23604,7 +21924,7 @@ export type operations = { * * **Credential required**: *No* */ - 'notes___search-by-tag': { + 'notes/search-by-tag': { requestBody: { content: { 'application/json': { @@ -23676,7 +21996,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___search: { + 'notes/search': { requestBody: { content: { 'application/json': { @@ -23687,6 +22007,11 @@ export type operations = { untilId?: string; /** @default 10 */ limit?: number; + /** + * @default combined + * @enum {string} + */ + origin?: 'local' | 'remote' | 'combined'; /** @default 0 */ offset?: number; /** @description The local host is represented with `.`. */ @@ -23701,15 +22026,6 @@ export type operations = { * @default null */ channelId?: string | null; - /** - * @default combined - * @enum {string} - */ - fileOption?: 'combined' | 'fileOnly' | 'noFile'; - /** @default false */ - excludeNsfw?: boolean; - /** @default false */ - excludeBot?: boolean; }; }; }; @@ -23758,7 +22074,7 @@ export type operations = { * * **Credential required**: *No* */ - notes___show: { + 'notes/show': { requestBody: { content: { 'application/json': { @@ -23812,7 +22128,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - notes___state: { + 'notes/state': { requestBody: { content: { 'application/json': { @@ -23869,7 +22185,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'notes___thread-muting___create': { + 'notes/thread-muting/create': { requestBody: { content: { 'application/json': { @@ -23927,7 +22243,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'notes___thread-muting___delete': { + 'notes/thread-muting/delete': { requestBody: { content: { 'application/json': { @@ -23979,7 +22295,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - notes___timeline: { + 'notes/timeline': { requestBody: { content: { 'application/json': { @@ -24005,8 +22321,6 @@ export type operations = { withRenotes?: boolean; /** @default false */ withCats?: boolean; - /** @default false */ - withoutBots?: boolean; }; }; }; @@ -24055,7 +22369,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - notes___translate: { + 'notes/translate': { requestBody: { content: { 'application/json': { @@ -24075,10 +22389,6 @@ export type operations = { }; }; }; - /** @description OK (without any results) */ - 204: { - content: never; - }; /** @description Client error */ 400: { content: { @@ -24117,7 +22427,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - notes___unrenote: { + 'notes/unrenote': { requestBody: { content: { 'application/json': { @@ -24175,7 +22485,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - 'notes___user-list-timeline': { + 'notes/user-list-timeline': { requestBody: { content: { 'application/json': { @@ -24254,7 +22564,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ - notifications___create: { + 'notifications/create': { requestBody: { content: { 'application/json': { @@ -24307,109 +22617,13 @@ export type operations = { }; }; }; - /** - * notifications/delete - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:notifications* - */ - notifications___delete: { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - notificationId: 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']; - }; - }; - }; - }; - /** - * notifications/flush - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *write:notifications* - */ - notifications___flush: { - 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']; - }; - }; - }; - }; /** * notifications/mark-all-as-read * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ - 'notifications___mark-all-as-read': { + 'notifications/mark-all-as-read': { responses: { /** @description OK (without any results) */ 204: { @@ -24453,7 +22667,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ - 'notifications___test-notification': { + 'notifications/test-notification': { responses: { /** @description OK (without any results) */ 204: { @@ -24558,7 +22772,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:pages* */ - pages___create: { + 'pages/create': { requestBody: { content: { 'application/json': { @@ -24637,7 +22851,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:pages* */ - pages___delete: { + 'pages/delete': { requestBody: { content: { 'application/json': { @@ -24689,7 +22903,7 @@ export type operations = { * * **Credential required**: *No* */ - pages___featured: { + 'pages/featured': { responses: { /** @description OK (with results) */ 200: { @@ -24735,7 +22949,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:page-likes* */ - pages___like: { + 'pages/like': { requestBody: { content: { 'application/json': { @@ -24787,7 +23001,7 @@ export type operations = { * * **Credential required**: *No* */ - pages___show: { + 'pages/show': { requestBody: { content: { 'application/json': { @@ -24843,7 +23057,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:page-likes* */ - pages___unlike: { + 'pages/unlike': { requestBody: { content: { 'application/json': { @@ -24895,22 +23109,22 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:pages* */ - pages___update: { + 'pages/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ pageId: string; - title?: string; - name?: string; + title: string; + name: string; summary?: string | null; - content?: { + content: { [key: string]: unknown; }[]; - variables?: { + variables: { [key: string]: unknown; }[]; - script?: string; + script: string; /** Format: misskey:id */ eyeCatchingImageId?: string | null; /** @enum {string} */ @@ -24969,7 +23183,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:flash* */ - flash___create: { + 'flash/create': { requestBody: { content: { 'application/json': { @@ -24977,11 +23191,6 @@ export type operations = { summary: string; script: string; permissions: string[]; - /** - * @default public - * @enum {string} - */ - visibility?: 'public' | 'private'; }; }; }; @@ -25036,7 +23245,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:flash* */ - flash___delete: { + 'flash/delete': { requestBody: { content: { 'application/json': { @@ -25088,7 +23297,7 @@ export type operations = { * * **Credential required**: *No* */ - flash___featured: { + 'flash/featured': { responses: { /** @description OK (with results) */ 200: { @@ -25135,7 +23344,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'flash___gen-token': { + 'flash/gen-token': { requestBody: { content: { 'application/json': { @@ -25196,7 +23405,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:flash-likes* */ - flash___like: { + 'flash/like': { requestBody: { content: { 'application/json': { @@ -25248,7 +23457,7 @@ export type operations = { * * **Credential required**: *No* */ - flash___show: { + 'flash/show': { requestBody: { content: { 'application/json': { @@ -25302,7 +23511,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:flash-likes* */ - flash___unlike: { + 'flash/unlike': { requestBody: { content: { 'application/json': { @@ -25354,16 +23563,16 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:flash* */ - flash___update: { + 'flash/update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ flashId: string; - title?: string; - summary?: string; - script?: string; - permissions?: string[]; + title: string; + summary: string; + script: string; + permissions: string[]; /** @enum {string} */ visibility?: 'public' | 'private'; }; @@ -25418,7 +23627,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:flash* */ - flash___my: { + 'flash/my': { requestBody: { content: { 'application/json': { @@ -25476,7 +23685,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:flash-likes* */ - 'flash___my-likes': { + 'flash/my-likes': { requestBody: { content: { 'application/json': { @@ -25632,7 +23841,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - promo___read: { + 'promo/read': { requestBody: { content: { 'application/json': { @@ -25684,7 +23893,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - roles___list: { + 'roles/list': { responses: { /** @description OK (with results) */ 200: { @@ -25730,7 +23939,7 @@ export type operations = { * * **Credential required**: *No* */ - roles___show: { + 'roles/show': { requestBody: { content: { 'application/json': { @@ -25784,7 +23993,7 @@ export type operations = { * * **Credential required**: *No* */ - roles___users: { + 'roles/users': { requestBody: { content: { 'application/json': { @@ -25806,7 +24015,7 @@ export type operations = { 'application/json': { /** Format: misskey:id */ id: string; - user: components['schemas']['UserDetailed']; + user: components['schemas']['User']; }[]; }; }; @@ -25848,7 +24057,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - roles___notes: { + 'roles/notes': { requestBody: { content: { 'application/json': { @@ -26178,7 +24387,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'sw___show-registration': { + 'sw/show-registration': { requestBody: { content: { 'application/json': { @@ -26240,7 +24449,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - 'sw___update-registration': { + 'sw/update-registration': { requestBody: { content: { 'application/json': { @@ -26299,7 +24508,7 @@ export type operations = { * **Internal Endpoint**: This endpoint is an API for the cherrypick mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ - sw___register: { + 'sw/register': { requestBody: { content: { 'application/json': { @@ -26363,7 +24572,7 @@ export type operations = { * * **Credential required**: *No* */ - sw___unregister: { + 'sw/unregister': { requestBody: { content: { 'application/json': { @@ -26435,12 +24644,12 @@ export type operations = { content: { 'application/json': { /** Format: misskey:id */ - id?: string; + id: string; required: boolean; - string?: string; - default?: string; + string: string; + default: string; /** @default hello */ - nullableDefault?: string | null; + nullableDefault: string | null; }; }; }; @@ -26482,7 +24691,7 @@ export type operations = { * * **Credential required**: *No* */ - username___available: { + 'username/available': { requestBody: { content: { 'application/json': { @@ -26610,7 +24819,7 @@ export type operations = { * * **Credential required**: *No* */ - users___clips: { + 'users/clips': { requestBody: { content: { 'application/json': { @@ -26670,7 +24879,7 @@ export type operations = { * * **Credential required**: *No* */ - users___followers: { + 'users/followers': { requestBody: { content: { 'application/json': { @@ -26733,7 +24942,7 @@ export type operations = { * * **Credential required**: *No* */ - users___following: { + 'users/following': { requestBody: { content: { 'application/json': { @@ -26797,7 +25006,7 @@ export type operations = { * * **Credential required**: *No* */ - users___gallery___posts: { + 'users/gallery/posts': { requestBody: { content: { 'application/json': { @@ -26857,7 +25066,7 @@ export type operations = { * * **Credential required**: *No* */ - 'users___get-frequently-replied-users': { + 'users/get-frequently-replied-users': { requestBody: { content: { 'application/json': { @@ -26916,7 +25125,7 @@ export type operations = { * * **Credential required**: *No* */ - 'users___featured-notes': { + 'users/featured-notes': { requestBody: { content: { 'application/json': { @@ -26974,7 +25183,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___create: { + 'users/groups/create': { requestBody: { content: { 'application/json': { @@ -27033,7 +25242,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___delete: { + 'users/groups/delete': { requestBody: { content: { 'application/json': { @@ -27085,7 +25294,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___invitations___accept: { + 'users/groups/invitations/accept': { requestBody: { content: { 'application/json': { @@ -27137,7 +25346,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___invitations___reject: { + 'users/groups/invitations/reject': { requestBody: { content: { 'application/json': { @@ -27189,7 +25398,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___invite: { + 'users/groups/invite': { requestBody: { content: { 'application/json': { @@ -27243,7 +25452,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:user-groups* */ - users___groups___joined: { + 'users/groups/joined': { responses: { /** @description OK (with results) */ 200: { @@ -27289,7 +25498,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___leave: { + 'users/groups/leave': { requestBody: { content: { 'application/json': { @@ -27341,7 +25550,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:user-groups* */ - users___groups___owned: { + 'users/groups/owned': { responses: { /** @description OK (with results) */ 200: { @@ -27387,7 +25596,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___pull: { + 'users/groups/pull': { requestBody: { content: { 'application/json': { @@ -27441,7 +25650,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:user-groups* */ - users___groups___show: { + 'users/groups/show': { requestBody: { content: { 'application/json': { @@ -27495,7 +25704,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___transfer: { + 'users/groups/transfer': { requestBody: { content: { 'application/json': { @@ -27551,7 +25760,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:user-groups* */ - users___groups___update: { + 'users/groups/update': { requestBody: { content: { 'application/json': { @@ -27606,7 +25815,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - users___lists___create: { + 'users/lists/create': { requestBody: { content: { 'application/json': { @@ -27659,7 +25868,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - users___lists___delete: { + 'users/lists/delete': { requestBody: { content: { 'application/json': { @@ -27711,7 +25920,7 @@ export type operations = { * * **Credential required**: *No* / **Permission**: *read:account* */ - users___lists___list: { + 'users/lists/list': { requestBody: { content: { 'application/json': { @@ -27765,7 +25974,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - users___lists___pull: { + 'users/lists/pull': { requestBody: { content: { 'application/json': { @@ -27819,7 +26028,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - users___lists___push: { + 'users/lists/push': { requestBody: { content: { 'application/json': { @@ -27879,7 +26088,7 @@ export type operations = { * * **Credential required**: *No* / **Permission**: *read:account* */ - users___lists___show: { + 'users/lists/show': { requestBody: { content: { 'application/json': { @@ -27935,7 +26144,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - users___lists___favorite: { + 'users/lists/favorite': { requestBody: { content: { 'application/json': { @@ -27987,7 +26196,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - users___lists___unfavorite: { + 'users/lists/unfavorite': { requestBody: { content: { 'application/json': { @@ -28039,7 +26248,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - users___lists___update: { + 'users/lists/update': { requestBody: { content: { 'application/json': { @@ -28095,7 +26304,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'users___lists___create-from-public': { + 'users/lists/create-from-public': { requestBody: { content: { 'application/json': { @@ -28150,7 +26359,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'users___lists___update-membership': { + 'users/lists/update-membership': { requestBody: { content: { 'application/json': { @@ -28205,7 +26414,7 @@ export type operations = { * * **Credential required**: *No* / **Permission**: *read:account* */ - 'users___lists___get-memberships': { + 'users/lists/get-memberships': { requestBody: { content: { 'application/json': { @@ -28233,7 +26442,7 @@ export type operations = { createdAt: string; /** Format: misskey:id */ userId: string; - user: components['schemas']['UserLite']; + user: components['schemas']['User']; withReplies: boolean; }[]; }; @@ -28276,7 +26485,7 @@ export type operations = { * * **Credential required**: *No* */ - users___notes: { + 'users/notes': { requestBody: { content: { 'application/json': { @@ -28288,8 +26497,6 @@ export type operations = { withRenotes?: boolean; /** @default false */ withChannelNotes?: boolean; - /** @default false */ - withoutBots?: boolean; /** @default 10 */ limit?: number; /** Format: misskey:id */ @@ -28352,7 +26559,7 @@ export type operations = { * * **Credential required**: *No* */ - users___pages: { + 'users/pages': { requestBody: { content: { 'application/json': { @@ -28412,7 +26619,7 @@ export type operations = { * * **Credential required**: *No* */ - users___flashs: { + 'users/flashs': { requestBody: { content: { 'application/json': { @@ -28472,7 +26679,7 @@ export type operations = { * * **Credential required**: *No* */ - users___reactions: { + 'users/reactions': { requestBody: { content: { 'application/json': { @@ -28534,7 +26741,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - users___recommendation: { + 'users/recommendation': { requestBody: { content: { 'application/json': { @@ -28590,7 +26797,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - users___relation: { + 'users/relation': { requestBody: { content: { 'application/json': { @@ -28665,7 +26872,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:report-abuse* */ - 'users___report-abuse': { + 'users/report-abuse': { requestBody: { content: { 'application/json': { @@ -28718,7 +26925,7 @@ export type operations = { * * **Credential required**: *No* */ - 'users___search-by-username-and-host': { + 'users/search-by-username-and-host': { requestBody: { content: { 'application/json': { @@ -28776,7 +26983,7 @@ export type operations = { * * **Credential required**: *No* */ - users___search: { + 'users/search': { requestBody: { content: { 'application/json': { @@ -28840,7 +27047,7 @@ export type operations = { * * **Credential required**: *No* */ - users___show: { + 'users/show': { requestBody: { content: { 'application/json': { @@ -28898,7 +27105,7 @@ export type operations = { * * **Credential required**: *No* */ - users___stats: { + 'users/stats': { requestBody: { content: { 'application/json': { @@ -28974,7 +27181,7 @@ export type operations = { * * **Credential required**: *No* */ - users___achievements: { + 'users/achievements': { requestBody: { content: { 'application/json': { @@ -29031,7 +27238,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *write:account* */ - 'users___update-memo': { + 'users/update-memo': { requestBody: { content: { 'application/json': { @@ -29085,7 +27292,7 @@ export type operations = { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - users___translate: { + 'users/translate': { requestBody: { content: { 'application/json': { @@ -29105,10 +27312,6 @@ export type operations = { }; }; }; - /** @description OK (without any results) */ - 204: { - content: never; - }; /** @description Client error */ 400: { content: { @@ -29160,52 +27363,7 @@ export type operations = { 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; - }; + items: Record[]; }; }; }; @@ -29316,133 +27474,7 @@ export type operations = { /** @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 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 To 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']; - }[]; + 'application/json': unknown; }; }; /** @description Client error */ diff --git a/packages/cherrypick-js/src/consts.ts b/packages/cherrypick-js/src/consts.ts index ea30c64158..0e446c1215 100644 --- a/packages/cherrypick-js/src/consts.ts +++ b/packages/cherrypick-js/src/consts.ts @@ -1,16 +1,6 @@ -import type { operations } from './autogen/types.js'; -import type { - AbuseReportNotificationRecipient, Ad, - Announcement, - EmojiDetailed, InviteCode, - MetaDetailed, - Note, - Role, SystemWebhook, UserLite, -} from './autogen/models.js'; - export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const; -export const noteVisibilities = ['public', 'home', 'followers', 'specified', 'private'] as const; +export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; @@ -68,6 +58,7 @@ export const permissions = [ 'read:admin:server-info', 'read:admin:show-moderation-log', 'read:admin:show-user', + 'read:admin:show-users', 'write:admin:suspend-user', 'write:admin:unset-user-avatar', 'write:admin:unset-user-banner', @@ -130,7 +121,6 @@ export const moderationLogTypes = [ 'resetPassword', 'suspendRemoteInstance', 'unsuspendRemoteInstance', - 'updateRemoteInstanceNote', 'markSensitiveDriveFile', 'unmarkSensitiveDriveFile', 'resolveAbuseReport', @@ -143,26 +133,12 @@ export const moderationLogTypes = [ 'deleteAvatarDecoration', 'unsetUserAvatar', 'unsetUserBanner', - 'createSystemWebhook', - 'updateSystemWebhook', - 'deleteSystemWebhook', - 'createAbuseReportNotificationRecipient', - 'updateAbuseReportNotificationRecipient', - 'deleteAbuseReportNotificationRecipient', ] as const; -type AvatarDecoration = UserLite['avatarDecorations'][number]; - -type ReceivedAbuseReport = { - reportId: AbuseReportNotificationRecipient['id']; - report: operations['admin___abuse-user-reports']['responses'][200]['content']['application/json']; - forwarded: boolean; -}; - export type ModerationLogPayloads = { updateServerSettings: { - before: MetaDetailed | null; - after: MetaDetailed | null; + before: any | null; + after: any | null; }; suspend: { userId: string; @@ -183,16 +159,16 @@ export type ModerationLogPayloads = { }; addCustomEmoji: { emojiId: string; - emoji: EmojiDetailed; + emoji: any; }; updateCustomEmoji: { emojiId: string; - before: EmojiDetailed; - after: EmojiDetailed; + before: any; + after: any; }; deleteCustomEmoji: { emojiId: string; - emoji: EmojiDetailed; + emoji: any; }; assignRole: { userId: string; @@ -211,16 +187,16 @@ export type ModerationLogPayloads = { }; createRole: { roleId: string; - role: Role; + role: any; }; updateRole: { roleId: string; - before: Role; - after: Role; + before: any; + after: any; }; deleteRole: { roleId: string; - role: Role; + role: any; }; clearQueue: Record; promoteQueue: Record; @@ -235,39 +211,39 @@ export type ModerationLogPayloads = { noteUserId: string; noteUserUsername: string; noteUserHost: string | null; - note: Note; + note: any; }; createGlobalAnnouncement: { announcementId: string; - announcement: Announcement; + announcement: any; }; createUserAnnouncement: { announcementId: string; - announcement: Announcement; + announcement: any; userId: string; userUsername: string; userHost: string | null; }; updateGlobalAnnouncement: { announcementId: string; - before: Announcement; - after: Announcement; + before: any; + after: any; }; updateUserAnnouncement: { announcementId: string; - before: Announcement; - after: Announcement; + before: any; + after: any; userId: string; userUsername: string; userHost: string | null; }; deleteGlobalAnnouncement: { announcementId: string; - announcement: Announcement; + announcement: any; }; deleteUserAnnouncement: { announcementId: string; - announcement: Announcement; + announcement: any; userId: string; userUsername: string; userHost: string | null; @@ -285,12 +261,6 @@ export type ModerationLogPayloads = { id: string; host: string; }; - updateRemoteInstanceNote: { - id: string; - host: string; - before: string | null; - after: string | null; - }; markSensitiveDriveFile: { fileId: string; fileUserId: string | null; @@ -305,37 +275,37 @@ export type ModerationLogPayloads = { }; resolveAbuseReport: { reportId: string; - report: ReceivedAbuseReport; + report: any; forwarded: boolean; }; createInvitation: { - invitations: InviteCode[]; + invitations: any[]; }; createAd: { adId: string; - ad: Ad; + ad: any; }; updateAd: { adId: string; - before: Ad; - after: Ad; + before: any; + after: any; }; deleteAd: { adId: string; - ad: Ad; + ad: any; }; createAvatarDecoration: { avatarDecorationId: string; - avatarDecoration: AvatarDecoration; + avatarDecoration: any; }; updateAvatarDecoration: { avatarDecorationId: string; - before: AvatarDecoration; - after: AvatarDecoration; + before: any; + after: any; }; deleteAvatarDecoration: { avatarDecorationId: string; - avatarDecoration: AvatarDecoration; + avatarDecoration: any; }; unsetUserAvatar: { userId: string; @@ -349,30 +319,4 @@ export type ModerationLogPayloads = { userHost: string | null; fileId: string; }; - createSystemWebhook: { - systemWebhookId: string; - webhook: SystemWebhook; - }; - updateSystemWebhook: { - systemWebhookId: string; - before: SystemWebhook; - after: SystemWebhook; - }; - deleteSystemWebhook: { - systemWebhookId: string; - webhook: SystemWebhook; - }; - createAbuseReportNotificationRecipient: { - recipientId: string; - recipient: AbuseReportNotificationRecipient; - }; - updateAbuseReportNotificationRecipient: { - recipientId: string; - before: AbuseReportNotificationRecipient; - after: AbuseReportNotificationRecipient; - }; - deleteAbuseReportNotificationRecipient: { - recipientId: string; - recipient: AbuseReportNotificationRecipient; - }; }; diff --git a/packages/cherrypick-js/src/entities.ts b/packages/cherrypick-js/src/entities.ts index ce58fb2970..99f433cc02 100644 --- a/packages/cherrypick-js/src/entities.ts +++ b/packages/cherrypick-js/src/entities.ts @@ -1,17 +1,8 @@ import { ModerationLogPayloads } from './consts.js'; -import { - Announcement, - EmojiDetailed, - MeDetailed, - Page, - Role, - RolePolicies, - User, - UserDetailedNotMe, -} from './autogen/models.js'; +import { Announcement, EmojiDetailed, Page, User, UserDetailed } from './autogen/models'; -export * from './autogen/entities.js'; -export * from './autogen/models.js'; +export * from './autogen/entities'; +export * from './autogen/models'; export type ID = string; export type DateString = string; @@ -19,7 +10,6 @@ export type DateString = string; export type PageEvent = { pageId: Page['id']; event: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any var: any; userId: User['id']; user: User; @@ -29,7 +19,7 @@ export type ModerationLog = { id: ID; createdAt: DateString; userId: User['id']; - user: UserDetailedNotMe | null; + user: UserDetailed | null; } & ({ type: 'updateServerSettings'; info: ModerationLogPayloads['updateServerSettings']; @@ -105,9 +95,6 @@ export type ModerationLog = { } | { type: 'unsuspendRemoteInstance'; info: ModerationLogPayloads['unsuspendRemoteInstance']; -} | { - type: 'updateRemoteInstanceNote'; - info: ModerationLogPayloads['updateRemoteInstanceNote']; } | { type: 'markSensitiveDriveFile'; info: ModerationLogPayloads['markSensitiveDriveFile']; @@ -142,23 +129,8 @@ export type ModerationLog = { type: 'unsetUserAvatar'; info: ModerationLogPayloads['unsetUserAvatar']; } | { - type: 'createSystemWebhook'; - info: ModerationLogPayloads['createSystemWebhook']; -} | { - type: 'updateSystemWebhook'; - info: ModerationLogPayloads['updateSystemWebhook']; -} | { - type: 'deleteSystemWebhook'; - info: ModerationLogPayloads['deleteSystemWebhook']; -} | { - type: 'createAbuseReportNotificationRecipient'; - info: ModerationLogPayloads['createAbuseReportNotificationRecipient']; -} | { - type: 'updateAbuseReportNotificationRecipient'; - info: ModerationLogPayloads['updateAbuseReportNotificationRecipient']; -} | { - type: 'deleteAbuseReportNotificationRecipient'; - info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient']; + type: 'unsetUserBanner'; + info: ModerationLogPayloads['unsetUserBanner']; }); export type ServerStats = { @@ -177,7 +149,7 @@ export type ServerStats = { } }; -export type ServerStatsLog = ServerStats[]; +export type ServerStatsLog = string[]; export type QueueStats = { deliver: { @@ -194,7 +166,7 @@ export type QueueStats = { }; }; -export type QueueStatsLog = QueueStats[]; +export type QueueStatsLog = string[]; export type EmojiAdded = { emoji: EmojiDetailed @@ -211,42 +183,3 @@ export type EmojiDeleted = { export type AnnouncementCreated = { announcement: Announcement; }; - -export type SignupRequest = { - username: string; - password: string; - host?: string; - invitationCode?: string; - emailAddress?: string; - 'hcaptcha-response'?: string | null; - 'g-recaptcha-response'?: string | null; - 'turnstile-response'?: string | null; -} - -export type SignupResponse = MeDetailed & { - token: string; -} - -export type SignupPendingRequest = { - code: string; -}; - -export type SignupPendingResponse = { - id: User['id'], - i: string, -}; - -export type SigninRequest = { - username: string; - password: string; - token?: string; -}; - -export type SigninResponse = { - id: User['id'], - i: string, -}; - -type Values> = T[keyof T]; - -export type PartialRolePolicyOverride = Partial<{[k in keyof RolePolicies]: Omit, 'value'> & { value: RolePolicies[k] }}>; diff --git a/packages/cherrypick-js/src/index.ts b/packages/cherrypick-js/src/index.ts index 28007a8ade..54cae8ec03 100644 --- a/packages/cherrypick-js/src/index.ts +++ b/packages/cherrypick-js/src/index.ts @@ -1,18 +1,15 @@ -import { type Endpoints } from './api.types.js'; +import { Endpoints } from './api.types.js'; import Stream, { Connection } from './streaming.js'; -import { type Channels } from './streaming.types.js'; -import { type Acct } from './acct.js'; +import { Channels } from './streaming.types.js'; +import { Acct } from './acct.js'; import * as consts from './consts.js'; -export type { - Endpoints, - Channels, - Acct, -}; - export { + Endpoints, Stream, Connection as ChannelConnection, + Channels, + Acct, }; export const permissions = consts.permissions; diff --git a/packages/cherrypick-js/src/streaming.ts b/packages/cherrypick-js/src/streaming.ts index a300b64500..bb53bf3d6c 100644 --- a/packages/cherrypick-js/src/streaming.ts +++ b/packages/cherrypick-js/src/streaming.ts @@ -1,9 +1,7 @@ import { EventEmitter } from 'eventemitter3'; -import _ReconnectingWebsocket from 'reconnecting-websocket'; +import ReconnectingWebsocket from 'reconnecting-websocket'; import type { BroadcastEvents, Channels } from './streaming.types.js'; -const ReconnectingWebsocket = _ReconnectingWebsocket as unknown as typeof _ReconnectingWebsocket['default']; - export function urlQuery(obj: Record): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) @@ -15,7 +13,7 @@ export function urlQuery(obj: Record> = T[keyof T]; +type AnyOf> = T[keyof T]; type StreamEvents = { _connected_: void; @@ -23,11 +21,10 @@ type StreamEvents = { } & BroadcastEvents; /** - * CherryPick stream connection + * Misskey stream connection */ -// eslint-disable-next-line import/no-default-export export default class Stream extends EventEmitter { - private stream: _ReconnectingWebsocket.default; + private stream: ReconnectingWebsocket; public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; private sharedConnectionPools: Pool[] = []; private sharedConnections: SharedConnection[] = []; @@ -35,7 +32,7 @@ export default class Stream extends EventEmitter { private idCounter = 0; constructor(origin: string, user: { token: string; } | null, options?: { - WebSocket?: WebSocket; + WebSocket?: any; }) { super(); @@ -52,7 +49,6 @@ export default class Stream extends EventEmitter { this.send = this.send.bind(this); this.close = this.close.bind(this); - // eslint-disable-next-line no-param-reassign options = options ?? { }; const query = urlQuery({ @@ -93,8 +89,8 @@ export default class Stream extends EventEmitter { this.sharedConnectionPools.push(pool); } - const connection = new SharedConnection(this, channel, pool, name); - this.sharedConnections.push(connection as unknown as SharedConnection); + const connection = new SharedConnection(this, channel, pool, name); + this.sharedConnections.push(connection); return connection; } @@ -108,7 +104,7 @@ export default class Stream extends EventEmitter { private connectToChannel(channel: C, params: Channels[C]['params']): NonSharedConnection { const connection = new NonSharedConnection(this, channel, this.genId(), params); - this.nonSharedConnections.push(connection as unknown as NonSharedConnection); + this.nonSharedConnections.push(connection); return connection; } @@ -176,9 +172,12 @@ export default class Stream extends EventEmitter { * ! ストリーム上のやり取りはすべてJSONで行われます ! */ public send(typeOrPayload: string): void - public send(typeOrPayload: string, payload: unknown): void - public send(typeOrPayload: Record | unknown[]): void - public send(typeOrPayload: string | Record | unknown[], payload?: unknown): void { + + public send(typeOrPayload: string, payload: any): void + + public send(typeOrPayload: Record | any[]): void + + public send(typeOrPayload: string | Record | any[], payload?: any): void { if (typeof typeOrPayload === 'string') { this.stream.send(JSON.stringify({ type: typeOrPayload, @@ -213,7 +212,7 @@ class Pool { public id: string; protected stream: Stream; public users = 0; - private disposeTimerId: ReturnType | null = null; + private disposeTimerId: any; private isConnected = false; constructor(stream: Stream, channel: string, id: string) { @@ -277,7 +276,7 @@ class Pool { } } -export abstract class Connection = AnyOf> extends EventEmitter { +export abstract class Connection = any> extends EventEmitter { public channel: string; protected stream: Stream; public abstract id: string; @@ -293,9 +292,7 @@ export abstract class Connection = AnyOf(type: T, body: Channel['receives'][T]): void { @@ -311,7 +308,7 @@ export abstract class Connection = AnyOf = AnyOf> extends Connection { +class SharedConnection = any> extends Connection { private pool: Pool; public get id(): string { @@ -330,11 +327,11 @@ class SharedConnection = AnyOf> extend public dispose(): void { this.pool.dec(); this.removeAllListeners(); - this.stream.removeSharedConnection(this as unknown as SharedConnection); + this.stream.removeSharedConnection(this); } } -class NonSharedConnection = AnyOf> extends Connection { +class NonSharedConnection = any> extends Connection { public id: string; protected params: Channel['params']; @@ -361,6 +358,6 @@ class NonSharedConnection = AnyOf> ext public dispose(): void { this.removeAllListeners(); this.stream.send('disconnect', { id: this.id }); - this.stream.disconnectToChannel(this as unknown as NonSharedConnection); + this.stream.disconnectToChannel(this); } } diff --git a/packages/cherrypick-js/src/streaming.types.ts b/packages/cherrypick-js/src/streaming.types.ts index 6e2941b933..5ff84b4ceb 100644 --- a/packages/cherrypick-js/src/streaming.types.ts +++ b/packages/cherrypick-js/src/streaming.types.ts @@ -2,14 +2,12 @@ import { Antenna, DriveFile, DriveFolder, + MeDetailed, Note, Notification, Signin, User, UserGroup, - UserDetailed, - UserDetailedNotMe, - UserLite, } from './autogen/models.js'; import { AnnouncementCreated, @@ -31,18 +29,16 @@ export type Channels = { mention: (payload: Note) => void; reply: (payload: Note) => void; renote: (payload: Note) => void; - follow: (payload: UserDetailedNotMe) => void; // 自分が他人をフォローしたとき - followed: (payload: UserDetailed | UserLite) => void; // 他人が自分をフォローしたとき - unfollow: (payload: UserDetailed) => void; // 自分が他人をフォロー解除したとき - meUpdated: (payload: UserDetailed) => void; + follow: (payload: User) => void; // 自分が他人をフォローしたとき + followed: (payload: User) => void; // 他人が自分をフォローしたとき + unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき + meUpdated: (payload: MeDetailed) => void; pageEvent: (payload: PageEvent) => void; urlUploadFinished: (payload: { marker: string; file: DriveFile; }) => void; readAllNotifications: () => void; unreadNotification: (payload: Notification) => void; unreadMention: (payload: Note['id']) => void; readAllUnreadMentions: () => void; - notificationFlushed: () => void; - notificationDeleted: () => void; unreadSpecifiedNote: (payload: Note['id']) => void; readAllUnreadSpecifiedNotes: () => void; readAllMessagingMessages: () => void; @@ -56,7 +52,6 @@ export type Channels = { registryUpdated: (payload: { scope?: string[]; key: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any | null; }) => void; driveFileCreated: (payload: DriveFile) => void; @@ -71,7 +66,6 @@ export type Channels = { withRenotes?: boolean; withFiles?: boolean; withCats?: boolean; - withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -84,7 +78,6 @@ export type Channels = { withReplies?: boolean; withFiles?: boolean; withCats?: boolean; - withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -97,7 +90,6 @@ export type Channels = { withReplies?: boolean; withFiles?: boolean; withCats?: boolean; - withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -109,7 +101,6 @@ export type Channels = { withRenotes?: boolean; withFiles?: boolean; withCats?: boolean; - withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -137,7 +128,6 @@ export type Channels = { params: { listId: string; withFiles?: boolean; - withRenotes?: boolean; withCats?: boolean; }; events: { @@ -189,7 +179,7 @@ export type Channels = { fileUpdated: (payload: DriveFile) => void; folderCreated: (payload: DriveFolder) => void; folderDeleted: (payload: DriveFolder['id']) => void; - folderUpdated: (payload: DriveFolder) => void; + folderUpdated: (payload: DriveFile) => void; }; receives: null; }; @@ -230,7 +220,7 @@ export type Channels = { } }; receives: null; - }; + } }; export type NoteUpdatedEvent = { diff --git a/packages/cherrypick-js/test-d/api.ts b/packages/cherrypick-js/test-d/api.ts index b17eb3058a..f9a2c63c39 100644 --- a/packages/cherrypick-js/test-d/api.ts +++ b/packages/cherrypick-js/test-d/api.ts @@ -1,5 +1,5 @@ import { expectType } from 'tsd'; -import * as Misskey from '../src/index.js'; +import * as Misskey from '../src'; describe('API', () => { test('success', async () => { diff --git a/packages/cherrypick-js/test-d/streaming.ts b/packages/cherrypick-js/test-d/streaming.ts index 6bab55bd6e..db87044c75 100644 --- a/packages/cherrypick-js/test-d/streaming.ts +++ b/packages/cherrypick-js/test-d/streaming.ts @@ -1,5 +1,5 @@ import { expectType } from 'tsd'; -import * as Misskey from '../src/index.js'; +import * as Misskey from '../src'; describe('Streaming', () => { test('emit type', async () => { diff --git a/packages/cherrypick-js/test/api.ts b/packages/cherrypick-js/test/api.ts index 8318f64d54..2aa9e52924 100644 --- a/packages/cherrypick-js/test/api.ts +++ b/packages/cherrypick-js/test/api.ts @@ -1,23 +1,17 @@ import { enableFetchMocks } from 'jest-fetch-mock'; -import { APIClient, isAPIError } from '../src/api.js'; +import { APIClient, isAPIError } from '../src/api'; enableFetchMocks(); function getFetchCall(call: any[]) { const { body, method } = call[1]; - const contentType = call[1].headers['Content-Type']; - if ( - body == null || - (contentType === 'application/json' && typeof body !== 'string') || - (contentType === 'multipart/form-data' && !(body instanceof FormData)) - ) { + if (body != null && typeof body != 'string') { throw new Error('invalid body'); } return { url: call[0], method: method, - contentType: contentType, - body: body instanceof FormData ? Object.fromEntries(body.entries()) : JSON.parse(body), + body: JSON.parse(body as any) }; } @@ -51,7 +45,6 @@ describe('API', () => { expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ url: 'https://cherrypick.test/api/i', method: 'POST', - contentType: 'application/json', body: { i: 'TOKEN' } }); }); @@ -85,52 +78,10 @@ describe('API', () => { expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ url: 'https://cherrypick.test/api/notes/show', method: 'POST', - contentType: 'application/json', body: { i: 'TOKEN', noteId: 'aaaaa' } }); }); - test('multipart/form-data', async () => { - fetchMock.resetMocks(); - fetchMock.mockResponse(async (req) => { - if (req.method == 'POST' && req.url == 'https://misskey.test/api/drive/files/create') { - if (req.headers.get('Content-Type')?.includes('multipart/form-data')) { - return JSON.stringify({ id: 'foo' }); - } else { - return { status: 400 }; - } - } else { - return { status: 404 }; - } - }); - - const cli = new APIClient({ - origin: 'https://misskey.test', - credential: 'TOKEN', - }); - - const testFile = new File([], 'foo.txt'); - - const res = await cli.request('drive/files/create', { - file: testFile, - name: null, // nullのパラメータは消える - }); - - expect(res).toEqual({ - id: 'foo' - }); - - expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ - url: 'https://misskey.test/api/drive/files/create', - method: 'POST', - contentType: 'multipart/form-data', - body: { - i: 'TOKEN', - file: testFile, - } - }); - }); - test('204 No Content で null が返る', async () => { fetchMock.resetMocks(); fetchMock.mockResponse(async (req) => { @@ -153,7 +104,6 @@ describe('API', () => { expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ url: 'https://cherrypick.test/api/reset-password', method: 'POST', - contentType: 'application/json', body: { i: 'TOKEN', token: 'aaa', password: 'aaa' } }); }); @@ -259,42 +209,4 @@ describe('API', () => { expect(isAPIError(e)).toEqual(false); } }); - - test('admin/roles/create の型が合う', async() => { - fetchMock.resetMocks(); - fetchMock.mockResponse(async () => { - return { - // 本来返すべき値は`Role`型だが、テストなのでお茶を濁す - status: 200, - body: '{}' - }; - }); - - const cli = new APIClient({ - origin: 'https://misskey.test', - credential: 'TOKEN', - }); - await cli.request('admin/roles/create', { - name: 'aaa', - asBadge: false, - canEditMembersByModerator: false, - color: '#123456', - condFormula: {}, - description: '', - displayOrder: 0, - iconUrl: '', - isAdministrator: false, - isExplorable: false, - isModerator: false, - isPublic: false, - policies: { - ltlAvailable: { - value: true, - priority: 0, - useDefault: false, - }, - }, - target: 'manual', - }); - }) }); diff --git a/packages/cherrypick-js/test/streaming.ts b/packages/cherrypick-js/test/streaming.ts index 8fbf050481..a8c05ce07c 100644 --- a/packages/cherrypick-js/test/streaming.ts +++ b/packages/cherrypick-js/test/streaming.ts @@ -1,5 +1,5 @@ import WS from 'jest-websocket-mock'; -import Stream from '../src/streaming.js'; +import Stream from '../src/streaming'; describe('Streaming', () => { test('useChannel', async () => { diff --git a/packages/cherrypick-js/tsconfig.json b/packages/cherrypick-js/tsconfig.json index f7bbc47304..f56b65e868 100644 --- a/packages/cherrypick-js/tsconfig.json +++ b/packages/cherrypick-js/tsconfig.json @@ -6,7 +6,7 @@ "moduleResolution": "nodenext", "declaration": true, "declarationMap": true, - "sourceMap": false, + "sourceMap": true, "outDir": "./built/", "removeComments": true, "strict": true, @@ -15,7 +15,6 @@ "experimentalDecorators": true, "noImplicitReturns": true, "esModuleInterop": true, - "exactOptionalPropertyTypes": true, "typeRoots": [ "./node_modules/@types" ], diff --git a/packages/frontend/.eslintrc.cjs b/packages/frontend/.eslintrc.cjs new file mode 100644 index 0000000000..d4cbb3a95b --- /dev/null +++ b/packages/frontend/.eslintrc.cjs @@ -0,0 +1,84 @@ +module.exports = { + root: true, + env: { + 'node': false, + }, + parser: 'vue-eslint-parser', + parserOptions: { + 'parser': '@typescript-eslint/parser', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + extraFileExtensions: ['.vue'], + }, + extends: [ + '../shared/.eslintrc.js', + 'plugin:vue/vue3-recommended', + 'plugin:storybook/recommended', + ], + rules: { + '@typescript-eslint/no-empty-interface': [ + 'error', + { + 'allowSingleExtends': true, + }, + ], + // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため + // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため + 'id-denylist': ['error', 'window', 'e'], + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + 'alphabetical': false, + }], + 'vue/no-use-v-if-with-v-for': ['error', { + 'allowUsingIterationVar': false, + }], + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + 'ignoreProperties': false, + }], + 'vue/no-v-html': 'warn', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + 'attribute': 1, + 'baseIndent': 0, + 'closeBracket': 0, + 'alignAttributesVertically': true, + 'ignores': [], + }], + 'vue/html-closing-bracket-spacing': ['warn', { + 'startTag': 'never', + 'endTag': 'never', + 'selfClosingTag': 'never', + }], + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/no-unused-vars': 'warn', + 'vue/no-dupe-keys': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-destructure': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }], + 'vue/attribute-hyphenation': ['error', 'never'], + }, + globals: { + // Node.js + 'module': false, + 'require': false, + '__dirname': false, + + // CherryPick + '_DEV_': false, + '_LANGS_': false, + '_VERSION_': false, + '_BASEDMISSKEYVERSION_': false, + '_ENV_': false, + '_PERF_PREFIX_': false, + '_DATA_TRANSFER_DRIVE_FILE_': false, + '_DATA_TRANSFER_DRIVE_FOLDER_': false, + '_DATA_TRANSFER_DECK_COLUMN_': false, + }, +}; diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts index f8fa8b34df..e82e44982e 100644 --- a/packages/frontend/.storybook/changes.ts +++ b/packages/frontend/.storybook/changes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -47,13 +47,14 @@ await fs.readFile( ) ) .map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, '')) + .map((path) => (path.startsWith('.') ? path : `./${path}`)) ); if ( micromatch(Array.from(modules), [ '../../assets/**', '../../fluent-emojis/**', - '../../locales/ja-JP.yml', '../../locales/ko-KR.yml', + '../../misskey-assets/**', 'assets/**', 'public/**', '../../pnpm-lock.yaml', diff --git a/packages/frontend/.storybook/charts.ts b/packages/frontend/.storybook/charts.ts deleted file mode 100644 index 5015012a82..0000000000 --- a/packages/frontend/.storybook/charts.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { DefaultBodyType, HttpResponse, HttpResponseResolver, JsonBodyType, PathParams, http } from 'msw'; -import seedrandom from 'seedrandom'; -import { action } from '@storybook/addon-actions'; - -function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] { - const rng = seedrandom(seed); - const max = Math.floor(option?.mul ?? 250 * rng()); - let accumulation = 0; - const array: number[] = []; - for (let i = 0; i < limit; i++) { - const num = Math.floor((max + 1) * rng()); - if (option?.accumulate) { - accumulation += num; - array.unshift(accumulation); - } else { - array.push(num); - } - } - return array; -} - -export function getChartResolver(fields: string[], option?: { accumulate?: boolean, mulMap?: Record }): HttpResponseResolver { - return ({ request }) => { - action(`GET ${request.url}`)(); - const limitParam = new URL(request.url).searchParams.get('limit'); - const limit = limitParam ? parseInt(limitParam) : 30; - const res = {}; - for (const field of fields) { - const layers = field.split('.'); - let current = res; - while (layers.length > 1) { - const currentKey = layers.shift()!; - if (current[currentKey] == null) current[currentKey] = {}; - current = current[currentKey]; - } - current[layers[0]] = getChartArray(field, limit, { - accumulate: option?.accumulate, - mul: option?.mulMap != null && field in option.mulMap ? option.mulMap[field] : undefined, - }); - } - return HttpResponse.json(res); - }; -} diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 94b909b40a..98cb8aa8af 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -22,61 +22,12 @@ export function abuseUserReport() { }; } -export function channel(id = 'somechannelid', name = 'Some Channel', bannerUrl: string | null = 'https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/fedi.jpg?raw=true'): entities.Channel { - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - lastNotedAt: '2016-12-28T22:49:51.000Z', - name, - description: null, - userId: null, - bannerUrl, - pinnedNoteIds: [], - color: '#000', - isArchived: false, - usersCount: 1, - notesCount: 1, - isSensitive: false, - allowRenoteToExternal: false, - }; -} - -export function clip(id = 'someclipid', name = 'Some Clip'): entities.Clip { - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - lastClippedAt: null, - userId: 'someuserid', - user: userLite(), - notesCount: undefined, - name, - description: 'Some clip description', - isPublic: false, - favoritedCount: 0, - }; -} - -export function emojiDetailed(id = 'someemojiid', name = 'some_emoji'): entities.EmojiDetailed { - return { - id, - aliases: ['alias1', 'alias2'], - name, - category: 'emojiCategory', - host: null, - url: '/client-assets/about-icon.png', - license: null, - isSensitive: false, - localOnly: false, - roleIdsThatCanBeUsedThisEmojiAsReaction: ['roleId1', 'roleId2'], - }; -} - export function galleryPost(isSensitive = false) { return { id: 'somepostid', createdAt: '2016-12-28T22:49:51.000Z', updatedAt: '2016-12-28T22:49:51.000Z', - userId: 'someuserid', + userid: 'someuserid', user: userDetailed(), title: 'Some post title', description: 'Some post description', @@ -114,65 +65,7 @@ export function file(isSensitive = false) { }; } -export function folder(id = 'somefolderid', name = 'Some Folder', parentId: string | null = null): entities.DriveFolder { - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - name, - parentId, - }; -} - -export function federationInstance(): entities.FederationInstance { - return { - id: 'someinstanceid', - firstRetrievedAt: '2021-01-01T00:00:00.000Z', - host: 'misskey-hub.net', - usersCount: 10, - notesCount: 20, - followingCount: 5, - followersCount: 15, - isNotResponding: false, - isSuspended: false, - suspensionState: 'none', - isBlocked: false, - softwareName: 'cherrypick', - softwareVersion: '4.9.0', - openRegistrations: false, - name: 'Misskey Hub', - description: '', - maintainerName: '', - maintainerEmail: '', - isSilenced: false, - iconUrl: 'https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/about-icon.png?raw=true', - faviconUrl: '', - themeColor: '', - infoUpdatedAt: '', - latestRequestReceivedAt: '', - }; -} - -export function note(id = 'somenoteid'): entities.Note { - return { - id, - createdAt: '2016-12-28T22:49:51.000Z', - deletedAt: null, - text: 'some note', - cw: null, - userId: 'someuserid', - user: userLite(), - visibility: 'public', - reactionAcceptance: 'nonSensitiveOnly', - reactionEmojis: {}, - reactions: {}, - myReaction: null, - reactionCount: 0, - renoteCount: 0, - repliesCount: 0, - }; -} - -export function userLite(id = 'someuserid', username = 'cherrypikist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'CherryPick User'): entities.UserLite { +export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'CherryPick User'): entities.UserDetailed { return { id, username, @@ -182,14 +75,9 @@ export function userLite(id = 'someuserid', username = 'cherrypikist', host: ent avatarUrl: 'https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/about-icon.png?raw=true', avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', avatarDecorations: [], - emojis: {}, - }; -} - -export function userDetailed(id = 'someuserid', username = 'cherrypikist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'CherryPick User'): entities.UserDetailed { - return { - ...userLite(id, username, host, name), + emojis: [], bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', + bannerColor: '#000000', bannerUrl: 'https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/fedi.jpg?raw=true', birthday: '2014-06-20', createdAt: '2016-12-28T22:49:51.000Z', @@ -230,16 +118,11 @@ export function userDetailed(id = 'someuserid', username = 'cherrypikist', host: publicReactions: false, securityKeys: false, twoFactorEnabled: false, - usePasswordLessLogin: false, twoFactorBackupCodesStock: 'none', updatedAt: null, - lastFetchedAt: null, uri: null, url: null, - movedTo: null, - alsoKnownAs: null, notify: 'none', - memo: null, }; } diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 406dc119f1..f47acb0d92 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -82,16 +82,23 @@ function h( return Object.assign(props || {}, { type }) as T; } -declare namespace h.JSX { - type Element = estree.Node; - type IntrinsicElements = { - [T in keyof typeof generator as ToKebab>>]: { - [K in keyof Omit< - Parameters<(typeof generator)[T]>[0], - 'type' - >]?: Parameters<(typeof generator)[T]>[0][K]; +declare global { + namespace JSX { + type Element = estree.Node; + type ElementClass = never; + type ElementAttributesProperty = never; + type ElementChildrenAttribute = never; + type IntrinsicAttributes = never; + type IntrinsicClassAttributes = never; + type IntrinsicElements = { + [T in keyof typeof generator as ToKebab>>]: { + [K in keyof Omit< + Parameters<(typeof generator)[T]>[0], + 'type' + >]?: Parameters<(typeof generator)[T]>[0][K]; + }; }; - }; + } } function toStories(component: string): Promise { @@ -381,7 +388,6 @@ function toStories(component: string): Promise { '/* eslint-disable @typescript-eslint/explicit-function-return-type */\n' + '/* eslint-disable import/no-default-export */\n' + '/* eslint-disable import/no-duplicates */\n' + - '/* eslint-disable import/order */\n' + generate(program, { generator }) + (hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''), { @@ -395,18 +401,16 @@ function toStories(component: string): Promise { // glob('src/{components,pages,ui,widgets}/**/*.vue') (async () => { const globs = await Promise.all([ - glob('src/components/global/Mk*.vue'), - glob('src/components/global/RouterView.vue'), - glob('src/components/Mk[A-E]*.vue'), + glob('src/components/global/*.vue'), + glob('src/components/Mk{A,B}*.vue'), + glob('src/components/MkDigitalClock.vue'), + glob('src/components/MkEvent.vue'), glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkUserSetupDialog.vue'), glob('src/components/MkUserSetupDialog.*.vue'), - glob('src/components/MkInstanceCardMini.vue'), glob('src/components/MkInviteCode.vue'), - glob('src/pages/search.vue'), glob('src/pages/user/home.vue'), - glob('src/components/global/CP*.vue'), ]); const components = globs.flat(); await Promise.all(components.map(async (component) => { diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index 9f318cf449..eb57078b39 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -1,31 +1,27 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { createRequire } from 'node:module'; -import { dirname, join, resolve } from 'node:path'; +import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { StorybookConfig } from '@storybook/vue3-vite'; import { type Plugin, mergeConfig } from 'vite'; import turbosnap from 'vite-plugin-turbosnap'; -const require = createRequire(import.meta.url); -const _dirname = fileURLToPath(new URL('.', import.meta.url)); +const dirname = fileURLToPath(new URL('.', import.meta.url)); const config = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], - staticDirs: [{ from: '../assets', to: '/client-assets' }], addons: [ - getAbsolutePath('@storybook/addon-essentials'), - getAbsolutePath('@storybook/addon-interactions'), - getAbsolutePath('@storybook/addon-links'), - getAbsolutePath('@storybook/addon-storysource'), - getAbsolutePath('@storybook/addon-mdx-gfm'), - resolve(_dirname, '../node_modules/storybook-addon-misskey-theme'), + '@storybook/addon-essentials', + '@storybook/addon-interactions', + '@storybook/addon-links', + '@storybook/addon-storysource', + resolve(dirname, '../node_modules/storybook-addon-misskey-theme'), ], framework: { - name: getAbsolutePath('@storybook/vue3-vite') as '@storybook/vue3-vite', + name: '@storybook/vue3-vite', options: {}, }, docs: { @@ -35,19 +31,16 @@ const config = { disableTelemetry: true, }, async viteFinal(config) { - const replacePluginForIsChromatic = config.plugins?.findIndex((plugin: Plugin) => plugin && plugin.name === 'replace') ?? -1; + const replacePluginForIsChromatic = config.plugins?.findIndex((plugin) => plugin && (plugin as Partial)?.name === 'replace') ?? -1; if (~replacePluginForIsChromatic) { config.plugins?.splice(replacePluginForIsChromatic, 1); } return mergeConfig(config, { plugins: [ - { - // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 - ...(turbosnap as any as typeof turbosnap['default'])({ - rootDir: config.root ?? process.cwd(), - }), - name: 'fake-turbosnap', - }, + // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 + (turbosnap as any as typeof turbosnap['default'])({ + rootDir: config.root ?? process.cwd(), + }), ], build: { target: [ @@ -60,7 +53,3 @@ const config = { }, } satisfies StorybookConfig; export default config; - -function getAbsolutePath(value: string): string { - return dirname(require.resolve(join(value, 'package.json'))); -} diff --git a/packages/frontend/.storybook/manager.ts b/packages/frontend/.storybook/manager.ts index 5b4b31de82..50a502e00d 100644 --- a/packages/frontend/.storybook/manager.ts +++ b/packages/frontend/.storybook/manager.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts index 29cb112ccb..fea11bc015 100644 --- a/packages/frontend/.storybook/mocks.ts +++ b/packages/frontend/.storybook/mocks.ts @@ -1,44 +1,31 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { type SharedOptions, http, HttpResponse } from 'msw'; +import { type SharedOptions, rest } from 'msw'; export const onUnhandledRequest = ((req, print) => { - const url = new URL(req.url); - if (url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(url.pathname)) { + if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) { return } print.warning() }) satisfies SharedOptions['onUnhandledRequest']; export const commonHandlers = [ - http.get('/fluent-emoji/:codepoints.png', async ({ params }) => { - const { codepoints } = params; + rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => { + const { codepoints } = req.params; const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob()); - return new HttpResponse(value, { - headers: { - 'Content-Type': 'image/png', - }, - }); + return res(ctx.set('Content-Type', 'image/png'), ctx.body(value)); }), - http.get('/fluent-emojis/:codepoints.png', async ({ params }) => { - const { codepoints } = params; + rest.get('/fluent-emojis/:codepoints.png', async (req, res, ctx) => { + const { codepoints } = req.params; const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob()); - return new HttpResponse(value, { - headers: { - 'Content-Type': 'image/png', - }, - }); + return res(ctx.set('Content-Type', 'image/png'), ctx.body(value)); }), - http.get('/twemoji/:codepoints.svg', async ({ params }) => { - const { codepoints } = params; + rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => { + const { codepoints } = req.params; const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); - return new HttpResponse(value, { - headers: { - 'Content-Type': 'image/svg+xml', - }, - }); + return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value)); }), ]; diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts index 85074b5bcc..9511c2ecad 100644 --- a/packages/frontend/.storybook/preload-locale.ts +++ b/packages/frontend/.storybook/preload-locale.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index e7ebbedd10..be0e980e5e 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index 2501204a35..537ccf971a 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -1,11 +1,6 @@ - - - + diff --git a/packages/frontend/src/components/MkCode.stories.impl.ts b/packages/frontend/src/components/MkCode.stories.impl.ts deleted file mode 100644 index b7e53e8e35..0000000000 --- a/packages/frontend/src/components/MkCode.stories.impl.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import MkCode from './MkCode.vue'; -const code = `for (let i, 100) { - <: if (i % 15 == 0) "FizzBuzz" - elif (i % 3 == 0) "Fizz" - elif (i % 5 == 0) "Buzz" - else i -}`; -export const Default = { - render(args) { - return { - components: { - MkCode, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - code, - lang: 'is', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index e381e6f91d..a45b3f1441 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -1,72 +1,58 @@ diff --git a/packages/frontend/src/components/MkCodeInline.stories.impl.ts b/packages/frontend/src/components/MkCodeInline.stories.impl.ts deleted file mode 100644 index 51d4d106ff..0000000000 --- a/packages/frontend/src/components/MkCodeInline.stories.impl.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import MkCodeInline from './MkCodeInline.vue'; -export const Default = { - render(args) { - return { - components: { - MkCodeInline, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - code: '<: "Hello, world!"', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue deleted file mode 100644 index 6add80d1bc..0000000000 --- a/packages/frontend/src/components/MkCodeInline.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkColorInput.stories.impl.ts b/packages/frontend/src/components/MkColorInput.stories.impl.ts deleted file mode 100644 index 61383e2cae..0000000000 --- a/packages/frontend/src/components/MkColorInput.stories.impl.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { action } from '@storybook/addon-actions'; -import MkColorInput from './MkColorInput.vue'; -export const Default = { - render(args) { - return { - components: { - MkColorInput, - }, - data() { - return { - color: '#cccccc', - }; - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'update:modelValue': action('update:modelValue'), - }; - }, - }, - template: '', - }; - }, - parameters: { - layout: 'fullscreen', - }, - decorators: [ - () => ({ - template: '
', - }), - ], -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue index f5c580789b..9a6f138cb0 100644 --- a/packages/frontend/src/components/MkColorInput.vue +++ b/packages/frontend/src/components/MkColorInput.vue @@ -1,5 +1,5 @@ @@ -41,8 +41,8 @@ const { modelValue } = toRefs(props); const v = ref(modelValue.value); const inputEl = shallowRef(); -const onInput = () => { - emit('update:modelValue', v.value ?? ''); +const onInput = (ev: KeyboardEvent) => { + emit('update:modelValue', v.value); }; diff --git a/packages/frontend/src/components/MkContainer.stories.impl.ts b/packages/frontend/src/components/MkContainer.stories.impl.ts deleted file mode 100644 index 72a7659521..0000000000 --- a/packages/frontend/src/components/MkContainer.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkContainer from './MkContainer.vue'; -void MkContainer; diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 0290e25fb6..32e186e505 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/components/MkContextMenu.stories.impl.ts b/packages/frontend/src/components/MkContextMenu.stories.impl.ts deleted file mode 100644 index 1ff0f51bd4..0000000000 --- a/packages/frontend/src/components/MkContextMenu.stories.impl.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { userEvent, within } from '@storybook/test'; -import MkContextMenu from './MkContextMenu.vue'; -import * as os from '@/os.js'; -export const Empty = { - render(args) { - return { - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - methods: { - onContextmenu(ev: MouseEvent) { - os.contextMenu(args.items, ev); - }, - }, - template: '
Right Click Here
', - }; - }, - args: { - items: [], - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const target = canvas.getByText('Right Click Here'); - await userEvent.pointer({ keys: '[MouseRight>]', target }); - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; -export const SomeTabs = { - ...Empty, - args: { - items: [ - { - text: 'Home', - icon: 'ti ti-home', - action() {}, - }, - ], - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index 8ea8fa6cf3..84172dcfb6 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -1,5 +1,5 @@ @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" >
- +
@@ -44,15 +44,15 @@ onMounted(() => { let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 - const width = rootEl.value!.offsetWidth; - const height = rootEl.value!.offsetHeight; + const width = rootEl.value.offsetWidth; + const height = rootEl.value.offsetHeight; - if (left + width - window.scrollX >= (window.innerWidth - SCROLLBAR_THICKNESS)) { - left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.scrollX; + if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) { + left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset; } - if (top + height - window.scrollY >= (window.innerHeight - SCROLLBAR_THICKNESS)) { - top = (window.innerHeight - SCROLLBAR_THICKNESS) - height + window.scrollY; + if (top + height - window.pageYOffset >= (window.innerHeight - SCROLLBAR_THICKNESS)) { + top = (window.innerHeight - SCROLLBAR_THICKNESS) - height + window.pageYOffset; } if (top < 0) { @@ -63,10 +63,8 @@ onMounted(() => { left = 0; } - if (rootEl.value) { - rootEl.value.style.top = `${top}px`; - rootEl.value.style.left = `${left}px`; - } + rootEl.value.style.top = `${top}px`; + rootEl.value.style.left = `${left}px`; document.body.addEventListener('mousedown', onMousedown); }); diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts deleted file mode 100644 index 4138ae1bde..0000000000 --- a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { action } from '@storybook/addon-actions'; -import { file } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import MkCropperDialog from './MkCropperDialog.vue'; -export const Default = { - render(args) { - return { - components: { - MkCropperDialog, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'ok': action('ok'), - 'cancel': action('cancel'), - 'closed': action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - file: file(), - aspectRatio: NaN, - }, - parameters: { - chromatic: { - // NOTE: ロードが終わるまで待つ - delay: 3000, - }, - layout: 'centered', - msw: { - handlers: [ - ...commonHandlers, - http.get('/proxy/image.webp', async ({ request }) => { - const url = new URL(request.url).searchParams.get('url'); - if (url === 'https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/fedi.jpg?raw=true') { - const image = await (await fetch('client-assets/fedi.jpg')).blob(); - return new HttpResponse(image, { - headers: { - 'Content-Type': 'image/jpeg', - }, - }); - } else { - return new HttpResponse(null, { status: 404 }); - } - }), - http.post('/api/drive/files/create', async ({ request }) => { - action('POST /api/drive/files/create')(await request.formData()); - return HttpResponse.json(file()); - }), - ], - }, - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index fcc7a5842f..a94d14bfd7 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -1,5 +1,5 @@ @@ -63,25 +63,18 @@ const loading = ref(true); const ok = async () => { const promise = new Promise(async (res) => { - const croppedImage = await cropper?.getCropperImage(); - const croppedSection = await cropper?.getCropperSelection(); - - // 拡大率を計算し、(ほぼ)元の大きさに戻す - const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth; - const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate; - - const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender }); + const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas(); croppedCanvas?.toBlob(blob => { if (!blob) return; const formData = new FormData(); formData.append('file', blob); formData.append('name', `cropped_${props.file.name}`); formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false'); - if (props.file.comment) { formData.append('comment', props.file.comment);} + formData.append('comment', props.file.comment ?? 'null'); formData.append('i', $i!.token); - if (props.uploadFolder) { - formData.append('folderId', props.uploadFolder); - } else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) { + if (props.uploadFolder || props.uploadFolder === null) { + formData.append('folderId', props.uploadFolder ?? 'null'); + } else if (defaultStore.state.uploadFolder) { formData.append('folderId', defaultStore.state.uploadFolder); } @@ -113,7 +106,6 @@ const onImageLoad = () => { loading.value = false; if (cropper) { - cropper.getCropperCanvas(); cropper.getCropperImage()!.$center('contain'); cropper.getCropperSelection()!.$center(); } @@ -160,7 +152,6 @@ onMounted(() => { width: var(--vw); height: var(--vh); position: relative; - object-fit: contain; > .loading { position: absolute; @@ -185,7 +176,6 @@ onMounted(() => { > ::v-deep(cropper-canvas) { width: 100%; height: 100%; - object-fit: contain; > cropper-selection > cropper-handle[action="move"] { background: transparent; diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts deleted file mode 100644 index 8a05e06311..0000000000 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { emojiDetailed } from '../../.storybook/fakes.js'; -import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; -export const Default = { - render(args) { - return { - components: { - MkCustomEmojiDetailedDialog, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - emoji: emojiDetailed(), - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue deleted file mode 100644 index 01898d5bd3..0000000000 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkCwButton.stories.impl.ts b/packages/frontend/src/components/MkCwButton.stories.impl.ts deleted file mode 100644 index 5d6ea56da9..0000000000 --- a/packages/frontend/src/components/MkCwButton.stories.impl.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { action } from '@storybook/addon-actions'; -import { expect, userEvent, within } from '@storybook/test'; -import { file } from '../../.storybook/fakes.js'; -import MkCwButton from './MkCwButton.vue'; -import { i18n } from '@/i18n.js'; - -export const Default = { - render(args) { - return { - components: { - MkCwButton, - }, - data() { - return { - showContent: false, - }; - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'update:modelValue': action('update:modelValue'), - }; - }, - }, - template: '', - }; - }, - args: { - text: 'Some CW content', - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const buttonElement = canvas.getByRole('button'); - await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); - await userEvent.click(buttonElement); - await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide); - await userEvent.click(buttonElement); - }, - parameters: { - chromatic: { - // NOTE: テストが終わるまで待つ - delay: 5000, - }, - layout: 'centered', - }, -} satisfies StoryObj; -export const IncludesTextAndDriveFile = { - ...Default, - args: { - text: 'Some CW content', - files: [file()], - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const buttonElement = canvas.getByRole('button'); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); - await expect(buttonElement).toHaveTextContent(' / '); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.files({ count: 1 })); - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index 3d169559d5..97c757ccd0 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -1,5 +1,5 @@ @@ -10,7 +10,6 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkDeprecatedWarning.vue b/packages/frontend/src/components/MkDeprecatedWarning.vue deleted file mode 100644 index 8bce6ca78f..0000000000 --- a/packages/frontend/src/components/MkDeprecatedWarning.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/MkDialog.stories.impl.ts b/packages/frontend/src/components/MkDialog.stories.impl.ts deleted file mode 100644 index 2d8d3661f2..0000000000 --- a/packages/frontend/src/components/MkDialog.stories.impl.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import { expect, userEvent, waitFor, within } from '@storybook/test'; -import { StoryObj } from '@storybook/vue3'; -import { i18n } from '@/i18n.js'; -import MkDialog from './MkDialog.vue'; -const Base = { - render(args) { - return { - components: { - MkDialog, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - done: action('done'), - closed: action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - text: 'Hello, world!', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; -export const Success = { - ...Base, - args: { - ...Base.args, - type: 'success', - }, -} satisfies StoryObj; -export const Error = { - ...Base, - args: { - ...Base.args, - type: 'error', - }, -} satisfies StoryObj; -export const Warning = { - ...Base, - args: { - ...Base.args, - type: 'warning', - }, -} satisfies StoryObj; -export const Info = { - ...Base, - args: { - ...Base.args, - type: 'info', - }, -} satisfies StoryObj; -export const Question = { - ...Base, - args: { - ...Base.args, - type: 'question', - }, -} satisfies StoryObj; -export const Waiting = { - ...Base, - args: { - ...Base.args, - type: 'waiting', - }, -} satisfies StoryObj; -export const DialogWithActions = { - ...Question, - args: { - ...Question.args, - text: i18n.ts.areYouSure, - actions: [ - { - text: i18n.ts.yes, - primary: true, - callback() { - action('YES')(); - }, - }, - { - text: i18n.ts.no, - callback() { - action('NO')(); - }, - }, - ], - }, -} satisfies StoryObj; -export const DialogWithDangerActions = { - ...Warning, - args: { - ...Warning.args, - text: i18n.ts.resetAreYouSure, - actions: [ - { - text: i18n.ts.yes, - danger: true, - primary: true, - callback() { - action('YES')(); - }, - }, - { - text: i18n.ts.no, - callback() { - action('NO')(); - }, - }, - ], - }, -} satisfies StoryObj; -export const DialogWithInput = { - ...Question, - args: { - ...Question.args, - title: 'Hello, world!', - text: undefined, - input: { - placeholder: i18n.ts.inputMessageHere, - type: 'text', - default: null, - minLength: 2, - maxLength: 3, - }, - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 0, min: 2 })); - const okButton = canvas.getByRole('button', { name: i18n.ts.ok }); - await expect(okButton).toBeDisabled(); - const input = canvas.getByRole('combobox'); - await waitFor(() => userEvent.hover(input)); - await waitFor(() => userEvent.click(input)); - await waitFor(() => userEvent.type(input, 'M')); - await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 1, min: 2 })); - await waitFor(() => userEvent.type(input, 'i')); - await expect(okButton).toBeEnabled(); - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 72a3764b73..5393f64319 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -1,10 +1,10 @@ diff --git a/packages/frontend/src/components/MkDigitalClock.stories.impl.ts b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts index e3391bcf7e..651297cd9a 100644 --- a/packages/frontend/src/components/MkDigitalClock.stories.impl.ts +++ b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue index 2e2321e6ac..a731307c2a 100644 --- a/packages/frontend/src/components/MkDigitalClock.vue +++ b/packages/frontend/src/components/MkDigitalClock.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/components/MkDivider.stories.impl.ts b/packages/frontend/src/components/MkDivider.stories.impl.ts deleted file mode 100644 index a593111987..0000000000 --- a/packages/frontend/src/components/MkDivider.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkDivider from './MkDivider.vue'; -void MkDivider; diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue deleted file mode 100644 index e4e3af99e4..0000000000 --- a/packages/frontend/src/components/MkDivider.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkDonation.stories.impl.ts b/packages/frontend/src/components/MkDonation.stories.impl.ts deleted file mode 100644 index 27d6b7df6c..0000000000 --- a/packages/frontend/src/components/MkDonation.stories.impl.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; -import { onBeforeUnmount } from 'vue'; -import MkDonation from './MkDonation.vue'; -import { instance } from '@/instance.js'; -export const Default = { - render(args) { - return { - components: { - MkDonation, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - closed: action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - // @ts-expect-error name is used for mocking instance - name: 'Misskey Hub', - }, - decorators: [ - (_, { args }) => ({ - setup() { - // @ts-expect-error name is used for mocking instance - instance.name = args.name; - onBeforeUnmount(() => instance.name = null); - }, - template: '', - }), - ], - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue index 19f5ae4b19..28b9484cbf 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/components/MkDrive.file.stories.impl.ts b/packages/frontend/src/components/MkDrive.file.stories.impl.ts deleted file mode 100644 index 5f6e6a0667..0000000000 --- a/packages/frontend/src/components/MkDrive.file.stories.impl.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; -import MkDrive_file from './MkDrive.file.vue'; -import { file } from '../../.storybook/fakes.js'; -export const Default = { - render(args) { - return { - components: { - MkDrive_file, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - chosen: action('chosen'), - dragstart: action('dragstart'), - dragend: action('dragend'), - }; - }, - }, - template: '', - }; - }, - args: { - file: file(), - }, - parameters: { - chromatic: { - // NOTE: ロードが終わるまで待つ - delay: 3000, - }, - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index cf4707a8ef..34f908df1d 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -1,5 +1,5 @@ @@ -45,9 +45,9 @@ import bytes from '@/filters/bytes.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; +import { useRouter } from '@/router.js'; import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; import { deviceKind } from '@/scripts/device-kind.js'; -import { useRouter } from '@/router/supplier.js'; const router = useRouter(); @@ -115,14 +115,14 @@ function onDragend() { background: rgba(#000, 0.05); > .label { - &::before, - &::after { + &:before, + &:after { background: #0b65a5; } &.red { - &::before, - &::after { + &:before, + &:after { background: #c12113; } } @@ -133,14 +133,14 @@ function onDragend() { background: rgba(#000, 0.1); > .label { - &::before, - &::after { + &:before, + &:after { background: #0b588c; } &.red { - &::before, - &::after { + &:before, + &:after { background: #ce2212; } } @@ -159,8 +159,8 @@ function onDragend() { } > .label { - &::before, - &::after { + &:before, + &:after { display: none; } } @@ -181,8 +181,8 @@ function onDragend() { left: 0; pointer-events: none; - &::before, - &::after { + &:before, + &:after { content: ""; display: block; position: absolute; @@ -190,14 +190,14 @@ function onDragend() { background: #0c7ac9; } - &::before { + &:before { top: 0; left: 57px; width: 28px; height: 8px; } - &::after { + &:after { top: 57px; left: 0; width: 8px; @@ -205,8 +205,8 @@ function onDragend() { } &.red { - &::before, - &::after { + &:before, + &:after { background: #c12113; } } diff --git a/packages/frontend/src/components/MkDrive.folder.stories.impl.ts b/packages/frontend/src/components/MkDrive.folder.stories.impl.ts deleted file mode 100644 index 97892f1d05..0000000000 --- a/packages/frontend/src/components/MkDrive.folder.stories.impl.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; -import { http, HttpResponse } from 'msw'; -import * as Misskey from 'cherrypick-js'; -import MkDrive_folder from './MkDrive.folder.vue'; -import { folder } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; -export const Default = { - render(args) { - return { - components: { - MkDrive_folder, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - chosen: action('chosen'), - move: action('move'), - upload: action('upload'), - removeFile: action('removeFile'), - removeFolder: action('removeFolder'), - dragstart: action('dragstart'), - dragend: action('dragend'), - }; - }, - }, - template: '', - }; - }, - args: { - folder: folder(), - }, - parameters: { - layout: 'centered', - msw: { - handlers: [ - ...commonHandlers, - http.post('/api/drive/folders/delete', async ({ request }) => { - action('POST /api/drive/folders/delete')(await request.json()); - return HttpResponse.json(undefined, { status: 204 }); - }), - http.post('/api/drive/folders/update', async ({ request }) => { - const req = await request.json() as Misskey.entities.DriveFoldersUpdateRequest; - action('POST /api/drive/folders/update')(req); - return HttpResponse.json({ - ...folder(), - id: req.folderId, - name: req.name ?? folder().name, - parentId: req.parentId ?? folder().parentId, - }); - }), - ], - }, - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 8a8fca02fa..9ae9e5579e 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -1,5 +1,5 @@ @@ -27,9 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts.uploadFolder }}

- + @@ -37,11 +35,10 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'cherrypick-js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ @@ -55,7 +52,6 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'chosen', v: Misskey.entities.DriveFolder): void; - (ev: 'unchose', v: Misskey.entities.DriveFolder): void; (ev: 'move', v: Misskey.entities.DriveFolder): void; (ev: 'upload', file: File, folder: Misskey.entities.DriveFolder); (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void; @@ -71,11 +67,7 @@ const isDragging = ref(false); const title = computed(() => props.folder.name); function checkboxClicked() { - if (props.isSelected) { - emit('unchose', props.folder); - } else { - emit('chosen', props.folder); - } + emit('chosen', props.folder); } function onClick() { @@ -152,7 +144,7 @@ function onDrop(ev: DragEvent) { if (driveFile != null && driveFile !== '') { const file = JSON.parse(driveFile); emit('removeFile', file.id); - misskeyApi('drive/files/update', { + os.api('drive/files/update', { fileId: file.id, folderId: props.folder.id, }); @@ -168,7 +160,7 @@ function onDrop(ev: DragEvent) { if (folder.id === props.folder.id) return; emit('removeFolder', folder.id); - misskeyApi('drive/folders/update', { + os.api('drive/folders/update', { folderId: folder.id, parentId: props.folder.id, }).then(() => { @@ -212,7 +204,7 @@ function onDragend() { } function go() { - emit('move', props.folder); + emit('move', props.folder.id); } function rename() { @@ -222,26 +214,15 @@ function rename() { default: props.folder.name, }).then(({ canceled, result: name }) => { if (canceled) return; - misskeyApi('drive/folders/update', { + os.api('drive/folders/update', { folderId: props.folder.id, name: name, }); }); } -function move() { - os.selectDriveFolder(false).then(folder => { - if (folder[0] && folder[0].id === props.folder.id) return; - - misskeyApi('drive/folders/update', { - folderId: props.folder.id, - parentId: folder[0] ? folder[0].id : null, - }); - }); -} - function deleteFolder() { - misskeyApi('drive/folders/delete', { + os.api('drive/folders/delete', { folderId: props.folder.id, }).then(() => { if (defaultStore.state.uploadFolder === props.folder.id) { @@ -275,20 +256,15 @@ function onContextmenu(ev: MouseEvent) { text: i18n.ts.openInWindow, icon: 'ti ti-app-window', action: () => { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), { + os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), { initialFolder: props.folder, }, { - closed: () => dispose(), - }); + }, 'closed'); }, }, { type: 'divider' }, { text: i18n.ts.rename, icon: 'ti ti-forms', action: rename, - }, { - text: i18n.ts.move, - icon: 'ti ti ti-folder-symlink', - action: move, }, { type: 'divider' }, { text: i18n.ts.delete, icon: 'ti ti-trash', @@ -319,7 +295,7 @@ function onContextmenu(ev: MouseEvent) { cursor: pointer; &.draghover { - &::after { + &:after { content: ""; pointer-events: none; position: absolute; @@ -333,43 +309,17 @@ function onContextmenu(ev: MouseEvent) { } } -.checkboxWrapper { +.checkbox { position: absolute; - border-radius: 50%; - bottom: 2px; - right: 2px; - padding: 8px; - box-sizing: border-box; - - > .checkbox { - position: relative; - width: 18px; - height: 18px; - background: #fff; - border: solid 2px var(--divider); - border-radius: 4px; - box-sizing: border-box; - - &.checked { - border-color: var(--accent); - background: var(--accent); - - &::after { - content: "\ea5e"; - font-family: 'tabler-icons'; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: #fff; - font-size: 12px; - line-height: 22px; - } - } - } - - &:hover { - background: var(--accentedBg); + bottom: 8px; + right: 8px; + width: 16px; + height: 16px; + background: #fff; + border: solid 1px #000; + + &.checked { + background: var(--accent); } } diff --git a/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts b/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts deleted file mode 100644 index 9d49f24fa4..0000000000 --- a/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkDrive_navFolder from './MkDrive.navFolder.vue'; -void MkDrive_navFolder; diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index ead091da5b..a246455e1f 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -1,5 +1,5 @@ @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkDriveWindow.stories.impl.ts b/packages/frontend/src/components/MkDriveWindow.stories.impl.ts deleted file mode 100644 index faa1f7fd5f..0000000000 --- a/packages/frontend/src/components/MkDriveWindow.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkDriveWindow from './MkDriveWindow.vue'; -void MkDriveWindow; diff --git a/packages/frontend/src/components/MkDriveWindow.vue b/packages/frontend/src/components/MkDriveWindow.vue index 1100cd2aaf..2f30c082e6 100644 --- a/packages/frontend/src/components/MkDriveWindow.vue +++ b/packages/frontend/src/components/MkDriveWindow.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts b/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts deleted file mode 100644 index 69aef577de..0000000000 --- a/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkEmojiPicker_section from './MkEmojiPicker.section.vue'; -void MkEmojiPicker_section; diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 26a1c78ea2..27fe0873ac 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -1,5 +1,5 @@ @@ -16,11 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only :key="emoji" :data-emoji="emoji" class="_button item" - :disabled="disabledEmojis?.value.includes(emoji)" @pointerenter="computeButtonTitle" @click="emit('chosen', emoji, $event)" > - + @@ -28,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- (: {{ customEmojiTree?.length }} : {{ emojis.length }}) + (:{{ customEmojiTree.length }} :{{ emojis.length }})
@@ -62,14 +60,13 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts b/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts deleted file mode 100644 index d38d8de808..0000000000 --- a/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import { expect, userEvent, waitFor, within } from '@storybook/test'; -import { StoryObj } from '@storybook/vue3'; -import { i18n } from '@/i18n.js'; -import MkEmojiPicker from './MkEmojiPicker.vue'; -export const Default = { - render(args) { - return { - components: { - MkEmojiPicker, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - chosen: action('chosen'), - }; - }, - }, - template: '', - }; - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const faceSection = canvas.getByText(/face/i); - await waitFor(() => userEvent.click(faceSection)); - const grinning = canvasElement.querySelector('[data-emoji="😀"]'); - await expect(grinning).toBeInTheDocument(); - if (grinning == null) throw new Error(); // NOTE: not called - await waitFor(() => userEvent.click(grinning)); - const recentUsedSection = canvas.getByText(new RegExp(i18n.ts.recentUsed)).parentElement; - await expect(recentUsedSection).toBeInTheDocument(); - if (recentUsedSection == null) throw new Error(); // NOTE: not called - await expect(within(recentUsedSection).getByAltText('😀')).toBeInTheDocument(); - await expect(within(recentUsedSection).queryByAltText('😬')).toEqual(null); - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 97a604caac..a1b56e7f56 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -1,23 +1,11 @@ + + diff --git a/packages/frontend/src/components/MkEvent.stories.impl.ts b/packages/frontend/src/components/MkEvent.stories.impl.ts index d1393420c3..0c1364ca28 100644 --- a/packages/frontend/src/components/MkEvent.stories.impl.ts +++ b/packages/frontend/src/components/MkEvent.stories.impl.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/components/MkEvent.vue b/packages/frontend/src/components/MkEvent.vue index 303a1ec900..1ac4026aee 100644 --- a/packages/frontend/src/components/MkEvent.vue +++ b/packages/frontend/src/components/MkEvent.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/components/MkEventEditor.vue b/packages/frontend/src/components/MkEventEditor.vue index 22928883c8..6a2d1ba5c4 100644 --- a/packages/frontend/src/components/MkEventEditor.vue +++ b/packages/frontend/src/components/MkEventEditor.vue @@ -1,5 +1,5 @@ @@ -125,6 +125,7 @@ import MkSwitch from './MkSwitch.vue'; import { formatDateTimeString } from '@/scripts/format-time-string.js'; import { addTime } from '@/scripts/time.js'; import { i18n } from '@/i18n.js'; +import date from '@/filters/date.js'; const props = defineProps<{ modelValue: Misskey.entities.Note['event'] diff --git a/packages/frontend/src/components/MkFeaturedPhotos.vue b/packages/frontend/src/components/MkFeaturedPhotos.vue index c42c692db0..addf439f3d 100644 --- a/packages/frontend/src/components/MkFeaturedPhotos.vue +++ b/packages/frontend/src/components/MkFeaturedPhotos.vue @@ -1,14 +1,22 @@ diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 124f114111..b86a5decd4 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -1,5 +1,5 @@ @@ -20,52 +20,41 @@ SPDX-License-Identifier: AGPL-3.0-only -
- @@ -79,24 +68,19 @@ import MkSelect from './MkSelect.vue'; import MkRange from './MkRange.vue'; import MkButton from './MkButton.vue'; import MkRadios from './MkRadios.vue'; -import XFile from './MkFormDialog.file.vue'; -import type { Form } from '@/scripts/form.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; -import { infoImageUrl } from '@/instance.js'; const props = defineProps<{ title: string; - form: Form; + form: any; }>(); const emit = defineEmits<{ (ev: 'done', v: { - canceled: true; - } | { - result: Record; + canceled?: boolean; + result?: any; }): void; - (ev: 'closed'): void; }>(); const dialog = shallowRef>(); @@ -110,13 +94,13 @@ function ok() { emit('done', { result: values, }); - dialog.value?.close(); + dialog.value.close(); } function cancel() { emit('done', { canceled: true, }); - dialog.value?.close(); + dialog.value.close(); } diff --git a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts index a433ad680b..ff3fecf4af 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts +++ b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts @@ -1,10 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { expect, userEvent, waitFor, within } from '@storybook/test'; +import { expect } from '@storybook/jest'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; import { StoryObj } from '@storybook/vue3'; import { galleryPost } from '../../.storybook/fakes.js'; import MkGalleryPostPreview from './MkGalleryPostPreview.vue'; diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 3cf1dd59cd..f8404aebc3 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -1,5 +1,5 @@ @@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only leaveActiveClass: $style.transition_toggle_leaveActive, leaveToClass: $style.transition_toggle_leaveTo, }" - :src="post.files?.[0]?.thumbnailUrl" - :hash="post.files?.[0]?.blurhash" + :src="post.files[0].thumbnailUrl" + :hash="post.files[0].blurhash" :forceBlurhash="!show" /> @@ -83,7 +83,7 @@ function leaveHover(): void { > article { > footer { - &::before { + &:before { opacity: 1; } } @@ -139,7 +139,7 @@ function leaveHover(): void { text-shadow: 0 0 8px #000; background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); - &::before { + &:before { content: ""; display: block; position: absolute; diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue index f75bd3de58..3f695c98b2 100644 --- a/packages/frontend/src/components/MkGoogle.vue +++ b/packages/frontend/src/components/MkGoogle.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index 84b571fb79..24f94e2218 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -1,5 +1,5 @@ @@ -15,8 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 40b78f3ef2..4106a85b1a 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -1,5 +1,5 @@ @@ -14,9 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" :leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined" > - - - + +
@@ -74,7 +73,7 @@ const props = withDefaults(defineProps<{ leaveFromClass?: string; } | null; src?: string | null; - hash?: string | null; + hash?: string; alt?: string | null; title?: string | null; height?: number; @@ -83,7 +82,6 @@ const props = withDefaults(defineProps<{ forceBlurhash?: boolean; onlyAvgColor?: boolean; // 軽量化のためにBlurhashを使わずに平均色だけを描画 noDrag?: boolean; - showAltIndicator?: boolean; }>(), { transition: null, src: null, @@ -95,7 +93,6 @@ const props = withDefaults(defineProps<{ forceBlurhash: false, onlyAvgColor: false, noDrag: false, - showAltIndicator: false, }); const viewId = uuid(); @@ -156,26 +153,22 @@ function drawImage(bitmap: CanvasImageSource) { } function drawAvg() { - if (!canvas.value) return; - - const color = (props.hash != null && extractAvgColorFromBlurhash(props.hash)) || '#888'; + if (!canvas.value || !props.hash) return; const ctx = canvas.value.getContext('2d'); if (!ctx) return; // avgColorでお茶をにごす ctx.beginPath(); - ctx.fillStyle = color; + ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888'; ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value); } async function draw() { - if (import.meta.env.MODE === 'test' && props.hash == null) return; + if (props.hash == null) return; drawAvg(); - if (props.hash == null) return; - if (props.onlyAvgColor) return; const work = await canvasPromise; @@ -272,21 +265,4 @@ onUnmounted(() => { -webkit-user-drag: none; } } - -.altIndicator { - display: flex; - gap: 4px; - position: absolute; - border-radius: 8px; - overflow: hidden; - top: 0; - right: 0; - background-color: var(--bg); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); - color: var(--accent); - font-size: 1em; - padding: 6px 8px; - text-align: center; -} diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index 61dc88b8e4..0f0c1fa9e2 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -1,5 +1,5 @@ @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index ddccdd0853..9ee9586636 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -1,5 +1,5 @@ @@ -22,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only :autocomplete="autocomplete" :autocapitalize="autocapitalize" :spellcheck="spellcheck" - :inputmode="inputmode" :step="step" :list="id" :min="min" @@ -64,7 +63,6 @@ const props = defineProps<{ mfmAutocomplete?: boolean | SuggestionType[], autocapitalize?: string; spellcheck?: boolean; - inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal'; step?: any; datalist?: string[]; min?: number; @@ -79,7 +77,7 @@ const props = defineProps<{ const emit = defineEmits<{ (ev: 'change', _ev: KeyboardEvent): void; (ev: 'keydown', _ev: KeyboardEvent): void; - (ev: 'enter', _ev: KeyboardEvent): void; + (ev: 'enter'): void; (ev: 'update:modelValue', value: string | number): void; }>(); @@ -90,18 +88,17 @@ const focused = ref(false); const changed = ref(false); const invalid = ref(false); const filled = computed(() => v.value !== '' && v.value != null); -const inputEl = shallowRef(); +const inputEl = shallowRef(); const prefixEl = shallowRef(); const suffixEl = shallowRef(); const height = props.small ? 33 : props.large ? 39 : 36; -let autocompleteWorker: Autocomplete | null = null; +let autocomplete: Autocomplete; -const focus = () => inputEl.value?.focus(); -const onInput = (event: Event) => { - const ev = event as KeyboardEvent; +const focus = () => inputEl.value.focus(); +const onInput = (ev: KeyboardEvent) => { changed.value = true; emit('change', ev); }; @@ -111,7 +108,7 @@ const onKeydown = (ev: KeyboardEvent) => { emit('keydown', ev); if (ev.code === 'Enter') { - emit('enter', ev); + emit('enter'); } }; @@ -124,9 +121,9 @@ const onFocus = () => { const updated = () => { changed.value = false; if (type.value === 'number') { - emit('update:modelValue', typeof v.value === 'number' ? v.value : parseFloat(v.value ?? '0')); + emit('update:modelValue', parseFloat(v.value)); } else { - emit('update:modelValue', v.value ?? ''); + emit('update:modelValue', v.value); } }; @@ -136,7 +133,7 @@ watch(modelValue, newValue => { v.value = newValue; }); -watch(v, () => { +watch(v, newValue => { if (!props.manualSave) { if (props.debounce) { debouncedUpdated(); @@ -145,14 +142,12 @@ watch(v, () => { } } - invalid.value = inputEl.value?.validity.badInput ?? true; + invalid.value = inputEl.value.validity.badInput; }); // このコンポーネントが作成された時、非表示状態である場合がある // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する useInterval(() => { - if (inputEl.value == null) return; - if (prefixEl.value) { if (prefixEl.value.offsetWidth) { inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; @@ -174,15 +169,15 @@ onMounted(() => { focus(); } }); - - if (props.mfmAutocomplete && inputEl.value) { - autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete); + + if (props.mfmAutocomplete) { + autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete); } }); onUnmounted(() => { - if (autocompleteWorker) { - autocompleteWorker.detach(); + if (autocomplete) { + autocomplete.detach(); } }); diff --git a/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts b/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts deleted file mode 100644 index 520db60d84..0000000000 --- a/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { HttpResponse, http } from 'msw'; -import { federationInstance } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import { getChartResolver } from '../../.storybook/charts.js'; -import MkInstanceCardMini from './MkInstanceCardMini.vue'; - -export const Default = { - render(args) { - return { - components: { - MkInstanceCardMini, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - instance: federationInstance(), - }, - parameters: { - layout: 'centered', - msw: { - handlers: [ - ...commonHandlers, - http.get('/undefined/preview.webp', async ({ request }) => { - const urlStr = new URL(request.url).searchParams.get('url'); - if (urlStr == null) { - return new HttpResponse(null, { status: 404 }); - } - const url = new URL(urlStr); - - if (url.href.startsWith('https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/')) { - const image = await (await fetch(`client-assets/${url.pathname.split('/').pop()}`)).blob(); - return new HttpResponse(image, { - headers: { - 'Content-Type': 'image/jpeg', - }, - }); - } else { - return new HttpResponse(null, { status: 404 }); - } - }), - http.get('/api/charts/instance', getChartResolver(['requests.received'])), - ], - }, - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index 1a22dd54fd..9fc1da6b5c 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -1,5 +1,5 @@ @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'cherrypick-js'; import MkMiniChart from '@/components/MkMiniChart.vue'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import * as os from '@/os.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ @@ -27,10 +27,10 @@ const props = defineProps<{ const chartValues = ref(null); -misskeyApiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => { +os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => { // 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く - res.requests.received.splice(0, 1); - chartValues.value = res.requests.received; + res['requests.received'].splice(0, 1); + chartValues.value = res['requests.received']; }); function getInstanceIcon(instance): string { diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index d74c885041..c337cf0703 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -1,5 +1,5 @@ @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -90,9 +90,8 @@ import MkSelect from '@/components/MkSelect.vue'; import MkChart from '@/components/MkChart.vue'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import * as os from '@/os.js'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import MkHeatmap, { type HeatmapSource } from '@/components/MkHeatmap.vue'; +import MkHeatmap from '@/components/MkHeatmap.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue'; @@ -103,7 +102,7 @@ initChart(); const chartLimit = 500; const chartSpan = ref<'hour' | 'day'>('hour'); const chartSrc = ref('active-users'); -const heatmapSrc = ref('active-users'); +const heatmapSrc = ref('active-users'); const subDoughnutEl = shallowRef(); const pubDoughnutEl = shallowRef(); @@ -138,8 +137,7 @@ function createDoughnut(chartEl, tooltip, data) { }, }, onClick: (ev) => { - if (ev.native == null) return; - const hit = chartInstance.getElementsAtEventForMode(ev.native, 'nearest', { intersect: true }, false)[0]; + const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0]; if (hit && data[hit.index].onClick) { data[hit.index].onClick(); } @@ -164,47 +162,24 @@ function createDoughnut(chartEl, tooltip, data) { } onMounted(() => { - misskeyApiGet('federation/stats', { limit: 30 }).then(fedStats => { - type ChartData = { - name: string, - color: string | null, - value: number, - onClick?: () => void, - }[]; - - const subs: ChartData = fedStats.topSubInstances.map(x => ({ + os.apiGet('federation/stats', { limit: 30 }).then(fedStats => { + createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount, onClick: () => { os.pageWindow(`/instance-info/${x.host}`); }, - })); - - subs.push({ - name: '(other)', - color: '#80808080', - value: fedStats.otherFollowersCount, - }); - - createDoughnut(subDoughnutEl.value, externalTooltipHandler1, subs); + })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }])); - const pubs: ChartData = fedStats.topPubInstances.map(x => ({ + createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount, onClick: () => { os.pageWindow(`/instance-info/${x.host}`); }, - })); - - pubs.push({ - name: '(other)', - color: '#80808080', - value: fedStats.otherFollowingCount, - }); - - createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, pubs); + })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }])); }); }); diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 206fd72a82..2ec5815dbb 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -1,5 +1,5 @@ @@ -18,9 +18,9 @@ import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ instance?: { - faviconUrl?: string | null - name?: string | null - themeColor?: string | null + faviconUrl?: string + name: string + themeColor?: string } }>(); @@ -30,7 +30,7 @@ const instance = props.instance ?? { themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content, }; -const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico'); +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'; diff --git a/packages/frontend/src/components/MkInviteCode.stories.impl.ts b/packages/frontend/src/components/MkInviteCode.stories.impl.ts index 456d215288..e1964ae08e 100644 --- a/packages/frontend/src/components/MkInviteCode.stories.impl.ts +++ b/packages/frontend/src/components/MkInviteCode.stories.impl.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors * SPDX-License-Identifier: AGPL-3.0-only */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; +import { rest } from 'msw'; import { userDetailed, inviteCode } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkInviteCode from './MkInviteCode.vue'; @@ -39,8 +39,8 @@ export const Default = { msw: { handlers: [ ...commonHandlers, - http.post('/api/users/show', ({ params }) => { - return HttpResponse.json(userDetailed(params.userId as string)); + rest.post('/api/users/show', (req, res, ctx) => { + return res(ctx.json(userDetailed(req.params.userId as string))); }), ], }, diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue index c3b38afdbb..3c27bce377 100644 --- a/packages/frontend/src/components/MkInviteCode.vue +++ b/packages/frontend/src/components/MkInviteCode.vue @@ -1,5 +1,5 @@ @@ -62,7 +62,7 @@ import { computed } from 'vue'; import * as Misskey from 'cherrypick-js'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue index 50c9e16e5e..66c42620ab 100644 --- a/packages/frontend/src/components/MkKeyValue.vue +++ b/packages/frontend/src/components/MkKeyValue.vue @@ -1,5 +1,5 @@ @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -119,7 +119,6 @@ function close() { margin-top: 12px; font-size: 0.8em; line-height: 1.5em; - text-align: center; } > .indicatorWithValue { @@ -139,7 +138,7 @@ function close() { left: 16px; color: var(--indicator); font-size: 8px; - animation: global-blink 1s infinite; + animation: blink 1s infinite; @media (max-width: 500px) { top: 16px; diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 07cf9e0c37..8aad6b9b1c 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -1,12 +1,11 @@