diff --git a/.config/docker_example.env b/.config/docker_example.env index 7a0261524..4fe8e76b7 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -2,3 +2,4 @@ POSTGRES_PASSWORD=example-misskey-pass POSTGRES_USER=example-misskey-user POSTGRES_DB=misskey +DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" diff --git a/.config/docker_example.yml b/.config/docker_example.yml index b3a6d7852..c6c83a98b 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -167,6 +167,9 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) +maxNoteLength: 3000 + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -197,6 +200,8 @@ proxyRemoteFiles: true # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true +# check that inbound ActivityPub GET requests are signed ("authorized fetch") +checkActivityPubGetSignature: false # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". diff --git a/.config/example.yml b/.config/example.yml index 28fe5b359..4aa7757c6 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -179,6 +179,9 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) +maxNoteLength: 3000 + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml index 155c5f7de..c31f3729b 100644 --- a/.forgejo/workflows/docker.yml +++ b/.forgejo/workflows/docker.yml @@ -41,7 +41,6 @@ jobs: type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} type=raw,value=stable - name: Log in to GHCR uses: https://github.com/docker/login-action@v3 diff --git a/.forgejo/workflows/lint.yml b/.forgejo/workflows/lint.yml index 0a773d5fb..e77c3e7a7 100644 --- a/.forgejo/workflows/lint.yml +++ b/.forgejo/workflows/lint.yml @@ -8,6 +8,12 @@ on: paths: - packages/** pull_request: + paths: + - packages/backend/** + - packages/frontend/** + - packages/sw/** + - packages/misskey-js/** + - packages/shared/.eslintrc.js jobs: pnpm_install: diff --git a/.gitea/ISSUE_TEMPLATE/02_feature-request.yml b/.gitea/ISSUE_TEMPLATE/02_feature-request.yml index d3bf64d86..b9f6d09aa 100644 --- a/.gitea/ISSUE_TEMPLATE/02_feature-request.yml +++ b/.gitea/ISSUE_TEMPLATE/02_feature-request.yml @@ -19,4 +19,4 @@ body: attributes: label: Do you want to implement this feature yourself? options: - - label: Yes, I will implement this by myself and send a pull request \ No newline at end of file + - label: Yes, I will implement this by myself and send a pull request diff --git a/.gitignore b/.gitignore index 11e69b262..216b5548e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ docker-compose.yml # misskey /build built +built-test /data /.cache-loader /db diff --git a/.gitmodules b/.gitmodules index 225a69a65..92d3a8753 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "fluent-emojis"] path = fluent-emojis url = https://github.com/misskey-dev/emojis.git +[submodule "tossface-emojis"] + path = tossface-emojis + url = https://git.joinsharkey.org/Sharkey/tossface-emojis.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e2e57b7..1d2c98ad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,27 @@ - +- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました +- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) +- Enhance: クリップをエクスポートできるように +- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正 ## 2023.12.2 diff --git a/COPYING b/COPYING index c218443d4..6a5f3ca1d 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2023 syuilo and contributers +Copyright © 2014-2024 syuilo and contributors And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. diff --git a/Dockerfile b/Dockerfile index 440c04e2d..2f2861960 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=21.4.0-alpine3.18 +ARG NODE_VERSION=20.10.0-alpine3.18 FROM node:${NODE_VERSION} as build @@ -44,6 +44,7 @@ COPY --from=build /sharkey/packages/megalodon/node_modules ./packages/megalodon/ COPY --from=build /sharkey/packages/misskey-js/built ./packages/misskey-js/built COPY --from=build /sharkey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules COPY --from=build /sharkey/fluent-emojis ./fluent-emojis +COPY --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist COPY --from=build /sharkey/sharkey-assets ./packages/frontend/assets COPY package.json ./package.json diff --git a/IMPORTANT_NOTES.md b/IMPORTANT_NOTES.md new file mode 100644 index 000000000..fba40d21c --- /dev/null +++ b/IMPORTANT_NOTES.md @@ -0,0 +1,13 @@ +# Basic Precautions + +When using a service with Sharkey, there are several important points to keep in mind. + +1. Because it is decentralized, there is no guarantee that data you upload will be deleted from all other servers even if you delete it once. (However, this applies to the internet in general.) + +2. Even for posts made in private, there is no guarantee that the recipient's server will treat them as private in the same way. Please exercise caution when posting personal or confidential information. (Again, this applies to the internet in general.) + +3. Account deletion can be a resource-intensive process and may take a long time. In cases with a lot of uploaded data, it may even be impossible to delete an account. + +4. Please disable ad blockers. Some servers may rely on advertising revenue to cover operating costs. Additionally, ad blockers can mistakenly block content and features unrelated to ads, potentially causing issues with the client's functionality and preventing normal use of Sharkey. Therefore, we recommend turning off ad blockers and similar features when using Sharkey. + +Please understand these points and enjoy using the service. \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index 3077c41e7..509ecb9fe 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,6 +6,7 @@ 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/docker-compose_example.yml b/docker-compose_example.yml index 5a8560bb4..6e291b459 100644 --- a/docker-compose_example.yml +++ b/docker-compose_example.yml @@ -2,13 +2,13 @@ version: "3" services: web: -# replace image below with git.joinsharkey.org/sharkey/sharkey:stable on next release -# image: ghcr.io/transfem-org/sharkey:stable +# image: git.joinsharkey.org/sharkey/sharkey:latest build: . restart: always links: - db - redis +# - mcaptcha # - meilisearch depends_on: db: @@ -49,12 +49,43 @@ services: 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 + # meilisearch: # restart: always # image: getmeili/meilisearch:v1.3.4 # environment: # - MEILI_NO_ANALYTICS=true # - MEILI_ENV=production +# - MEILI_MASTER_KEY=ChangeThis # networks: # - shonk # volumes: diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 83b254b2d..fd25ba028 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -973,6 +973,7 @@ neverShow: "Nicht wieder anzeigen" remindMeLater: "Vielleicht später" didYouLikeMisskey: "Gefällt dir Sharkey?" pleaseDonate: "Sharkey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" +pleaseDonateInstance: "Du kannst {host} auch direkt unterstützen, indem du an deine Instanz Administration spendest." roles: "Rollen" role: "Rolle" noRole: "Rolle nicht gefunden" @@ -1150,6 +1151,8 @@ impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, privacyPolicy: "Datenschutzerklärung" privacyPolicyUrl: "Datenschutzerklärungs-URL" tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung" +donation: "Spenden" +donationUrl: "Spenden-URL" avatarDecorations: "Profilbilddekoration" attach: "Anbringen" detach: "Entfernen" diff --git a/locales/en-US.yml b/locales/en-US.yml index 1ac2f4b28..7a28ce109 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -126,7 +126,11 @@ add: "Add" reaction: "Reactions" reactions: "Reactions" emojiPicker: "Emoji picker" +pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately 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" @@ -955,6 +959,10 @@ numberOfPageCache: "Number of cached pages" numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device." numberOfReplies: "Number of replies in a thread" numberOfRepliesDescription: "Increasing this number will display more replies. Setting this too high can cause replies to be cramped and unreadable." +boostSettings: "Boost Settings" +showVisibilitySelectorOnBoost: "Show Visibility Selector" +showVisibilitySelectorOnBoostDescription: "Shows the visiblity selector if enabled when clicking boost, if disabled it will use the default visiblity defined below and the selector will not show up." +visibilityOnBoost: "Default boost visibility" logoutConfirm: "Really log out?" lastActiveDate: "Last used at" statusbar: "Status bar" @@ -1006,6 +1014,7 @@ neverShow: "Don't show again" remindMeLater: "Maybe later" didYouLikeMisskey: "Have you taken a liking to Sharkey?" pleaseDonate: "{host} uses the free software, Sharkey. We would highly appreciate your donations so development of Sharkey can continue!" +pleaseDonateInstance: "You can also support {host} directly by donating to your instance administration." roles: "Roles" role: "Role" noRole: "Role not found" @@ -1192,9 +1201,12 @@ impressumDescription: "In some countries, like germany, the inclusion of operato privacyPolicy: "Privacy Policy" privacyPolicyUrl: "Privacy Policy URL" tosAndPrivacyPolicy: "Terms of Service and Privacy Policy" +donation: "Donate" +donationUrl: "Donation URL" avatarDecorations: "Avatar decorations" attach: "Attach" detach: "Remove" +detachAll: "Remove all" angle: "Angle" flip: "Flip" showAvatarDecorations: "Show avatar decorations" @@ -1208,7 +1220,12 @@ 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 effects" decorate: "Decorate" +addMfmFunction: "Add MFM" +enableQuickAddMfmFunction: "Show advanced MFM picker" _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." @@ -1642,6 +1659,7 @@ _role: canHideAds: "Can hide ads" canSearchNotes: "Usage of note search" canUseTranslator: "Translator usage" + avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" _condition: isLocal: "Local user" isRemote: "Remote user" @@ -1670,6 +1688,7 @@ _emailUnavailable: disposable: "Disposable email addresses may not be used" mx: "This email server is invalid" smtp: "This email server is not responding" + banned: "This email address is banned" _ffVisibility: public: "Public" followers: "Visible to followers only" @@ -1964,6 +1983,55 @@ _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 account" + "write:admin:delete-all-files-of-a-user": "Delete all files of a user" + "read:admin:index-stats": "View information about database indexes" + "read:admin:table-stats": "View information about database tables" + "read:admin:user-ips": "View user IP address" + "read:admin:meta": "View instance metadata" + "write:admin:reset-password": "Reset user passwords" + "write:admin:resolve-abuse-user-report": "Resolve user reports" + "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 user information" + "read:admin:show-users": "View users" + "write:admin:suspend-user": "Suspend user" + "write:admin:unset-user-avatar": "Remove avatar from user" + "write:admin:unset-user-banner": "Remove banner from user" + "write:admin:unsuspend-user": "Unsuspend user" + "write:admin:meta": "Edit instance metadata" + "write:admin:user-note": "Edit user note" + "write:admin:roles": "Edit roles" + "read:admin:roles": "View roles" + "write:admin:relays": "Edit relays" + "read:admin:relays": "View relays" + "write:admin:invite-codes": "Edit invite codes" + "read:admin:invite-codes": "View invite codes" + "write:admin:announcements": "Edit announcements" + "read:admin:announcements": "View announcements" + "write:admin:avatar-decorations": "Edit avatar decorations" + "read:admin:avatar-decorations": "View avatar decorations" + "write:admin:federation": "Edit remote instance information" + "write:admin:account": "Edit users" + "read:admin:account": "View information about user" + "write:admin:emoji": "Edit emojis" + "read:admin:emoji": "View emojis" + "write:admin:queue": "Edit queue" + "read:admin:queue": "View queue" + "write:admin:promo": "Edit promo" + "write:admin:drive": "Edit user drive" + "read:admin:drive": "View user drive" + "read:admin:stream": "Using the Websocket API for Admin" + "write:admin:ad": "Edit ads" + "read:admin:ad": "View ads" + "write:invite-codes": "Create Invitation Code" + "read:invite-codes": "View Invitation Code" + "write:clip-favorite": "Edit clips and likes" + "read:clip-favorite": "View clips and likes" + "read:federation": "View information about remote instance" + "write:report-abuse": "Report abuse" _auth: shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" @@ -2082,8 +2150,13 @@ _profile: metadataContent: "Content" changeAvatar: "Change avatar" changeBanner: "Change banner" + updateBanner: "Update banner" + removeBanner: "Remove banner" changeBackground: "Change background" + updateBackground: "Update background" + removeBackground: "Remove background" 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" @@ -2464,11 +2537,13 @@ _dataRequest: _dataSaver: _media: title: "Loading Media" + description: "Prevents images/videos from being loaded automatically. Hidden images/videos will be loaded when tapped." _avatar: title: "Avatar image" 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." _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." diff --git a/locales/index.d.ts b/locales/index.d.ts index dd2f34a69..e36ff1983 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -393,6 +393,11 @@ export interface Locale { "enableHcaptcha": string; "hcaptchaSiteKey": string; "hcaptchaSecretKey": string; + "mcaptcha": string; + "enableMcaptcha": string; + "mcaptchaSiteKey": string; + "mcaptchaSecretKey": string; + "mcaptchaInstanceUrl": string; "recaptcha": string; "enableRecaptcha": string; "recaptchaSiteKey": string; @@ -643,6 +648,7 @@ export interface Locale { "small": string; "generateAccessToken": string; "permission": string; + "adminPermission": string; "enableAll": string; "disableAll": string; "tokenRequested": string; @@ -686,6 +692,7 @@ export interface Locale { "other": string; "regenerateLoginToken": string; "regenerateLoginTokenDescription": string; + "theKeywordWhenSearchingForCustomEmoji": string; "setMultipleBySeparatingWithSpace": string; "fileIdOrUrl": string; "behavior": string; @@ -962,6 +969,10 @@ export interface Locale { "numberOfPageCacheDescription": string; "numberOfReplies": string; "numberOfRepliesDescription": string; + "boostSettings": string; + "showVisibilitySelectorOnBoost": string; + "showVisibilitySelectorOnBoostDescription": string; + "visibilityOnBoost": string; "logoutConfirm": string; "lastActiveDate": string; "statusbar": string; @@ -1013,6 +1024,7 @@ export interface Locale { "remindMeLater": string; "didYouLikeMisskey": string; "pleaseDonate": string; + "pleaseDonateInstance": string; "roles": string; "role": string; "noRole": string; @@ -1199,6 +1211,8 @@ export interface Locale { "privacyPolicy": string; "privacyPolicyUrl": string; "tosAndPrivacyPolicy": string; + "donation": string; + "donationUrl": string; "avatarDecorations": string; "attach": string; "detach": string; @@ -1222,6 +1236,21 @@ export interface Locale { "decorate": string; "addMfmFunction": string; "enableQuickAddMfmFunction": string; + "bubbleGame": string; + "sfx": string; + "soundWillBePlayed": string; + "showReplay": string; + "replay": string; + "replaying": string; + "ranking": string; + "_bubbleGame": { + "howToPlay": string; + "_howToPlay": { + "section1": string; + "section2": string; + "section3": string; + }; + }; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -1686,6 +1715,15 @@ export interface Locale { "title": string; "description": string; }; + "_bubbleGameExplodingHead": { + "title": string; + "description": string; + }; + "_bubbleGameDoubleExplodingHead": { + "title": string; + "description": string; + "flavor": string; + }; }; }; "_role": { @@ -2287,13 +2325,18 @@ export interface Locale { "metadataContent": string; "changeAvatar": string; "changeBanner": string; + "updateBanner": string; + "removeBanner": string; "changeBackground": string; + "updateBackground": string; + "removeBackground": string; "verifiedLinkDescription": string; "avatarDecorationMax": string; }; "_exportOrImport": { "allNotes": string; "favoritedNotes": string; + "clips": string; "followingList": string; "muteList": string; "blockingList": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b632fbad6..5609cacb2 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -15,7 +15,7 @@ gotIt: "わかった" cancel: "キャンセル" noThankYou: "やめておく" enterUsername: "ユーザー名を入力" -renotedBy: "{user}がリノート" +renotedBy: "{user}がブースト" noNotes: "ノートはありません" noNotifications: "通知はありません" instance: "サーバー" @@ -46,16 +46,16 @@ pin: "ピン留め" unpin: "ピン留め解除" copyContent: "内容をコピー" copyLink: "リンクをコピー" -copyLinkRenote: "リノートのリンクをコピー" +copyLinkRenote: "ブーストのリンクをコピー" delete: "削除" deleteAndEdit: "削除して編集" -deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、リノート、返信も全て削除されます。" +deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、ブースト、返信も全て削除されます。" addToList: "リストに追加" addToAntenna: "アンテナに追加" sendMessage: "メッセージを送信" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" -openRemoteProfile: "リモートプロファイルを開く" +openRemoteProfile: "リモートプロフィールを開く" copyUserId: "ユーザーIDをコピー" copyNoteId: "ノートIDをコピー" copyFileId: "ファイルIDをコピー" @@ -107,15 +107,15 @@ followRequests: "フォロー申請" unfollow: "フォロー解除" followRequestPending: "フォロー許可待ち" enterEmoji: "絵文字を入力" -renote: "リノート" -unrenote: "リノート解除" -renoted: "ブースト。" +renote: "ブースト" +unrenote: "ブースト解除" +renoted: "ブーストしました。" quoted: "引用。" -rmboost: "アンブースト。" -cantRenote: "この投稿はリノートできません。" -cantReRenote: "リノートをリノートすることはできません。" +rmboost: "ブースト解除しました。" +cantRenote: "この投稿はブーストできません。" +cantReRenote: "ブーストをブーストすることはできません。" quote: "引用" -inChannelRenote: "チャンネル内リノート" +inChannelRenote: "チャンネル内ブースト" inChannelQuote: "チャンネル内引用" pinnedNote: "ピン留めされたノート" pinned: "ピン留め" @@ -139,8 +139,8 @@ unmarkAsSensitive: "センシティブを解除する" enterFileName: "ファイル名を入力" mute: "ミュート" unmute: "ミュート解除" -renoteMute: "リノートをミュート" -renoteUnmute: "リノートのミュートを解除" +renoteMute: "ブーストをミュート" +renoteUnmute: "ブーストのミュートを解除" block: "ブロック" unblock: "ブロック解除" markAsNSFW: "ユーザーのすべてのメディアをNSFWとしてマークする" @@ -209,8 +209,8 @@ charts: "チャート" perHour: "1時間ごと" perDay: "1日ごと" stopActivityDelivery: "アクティビティの配送を停止" -blockThisInstance: "このサーバーをブロック" -silenceThisInstance: "サーバーをサイレンス" +blockThisInstance: "このインスタンスをブロック" +silenceThisInstance: "インスタンスをサイレンス" operations: "操作" software: "ソフトウェア" version: "バージョン" @@ -231,7 +231,7 @@ clearCachedFilesConfirm: "キャッシュされたリモートファイルをす blockedInstances: "ブロックしたサーバー" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。" silencedInstances: "サイレンスしたサーバー" -silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。" +silencedInstancesDescription: "サイレンスしたいインスタンスのホストを改行で区切って設定します。サイレンスされたインスタンスに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" @@ -390,6 +390,11 @@ hcaptcha: "hCaptcha" enableHcaptcha: "hCaptchaを有効にする" hcaptchaSiteKey: "サイトキー" hcaptchaSecretKey: "シークレットキー" +mcaptcha: "mCaptcha" +enableMcaptcha: "mCaptchaを有効にする" +mcaptchaSiteKey: "サイトキー" +mcaptchaSecretKey: "シークレットキー" +mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHAを有効にする" recaptchaSiteKey: "サイトキー" @@ -640,6 +645,7 @@ medium: "中" small: "小" generateAccessToken: "アクセストークンの発行" permission: "権限" +adminPermission: "管理者権限" enableAll: "全て有効にする" disableAll: "全て無効にする" tokenRequested: "アカウントへのアクセス許可" @@ -683,13 +689,14 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使 other: "その他" regenerateLoginToken: "ログイントークンを再生成" regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" +theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" fileIdOrUrl: "ファイルIDまたはURL" behavior: "動作" sample: "サンプル" abuseReports: "通報" reportAbuse: "通報" -reportAbuseRenote: "リノートを通報" +reportAbuseRenote: "ブーストを通報" reportAbuseOf: "{name}を通報する" fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。" abuseReported: "内容が送信されました。ご報告ありがとうございました。" @@ -723,9 +730,9 @@ manageAccessTokens: "アクセストークンの管理" accountInfo: "アカウント情報" notesCount: "ノートの数" repliesCount: "返信した数" -renotesCount: "リノートした数" +renotesCount: "ブーストした数" repliedCount: "返信された数" -renotedCount: "リノートされた数" +renotedCount: "ブーストされた数" followingCount: "フォロー数" followersCount: "フォロワー数" sentReactionsCount: "リアクションした数" @@ -959,6 +966,10 @@ numberOfPageCache: "ページキャッシュ数" numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" numberOfReplies: "スレッド内の返信数" numberOfRepliesDescription: "この数値を大きくすると、より多くの返信が表示されます。この値を大きくしすぎると、返信が窮屈になり、読めなくなることがあります。" +boostSettings: "ブースト設定" +showVisibilitySelectorOnBoost: "可視性セレクタを表示" +showVisibilitySelectorOnBoostDescription: "無効の場合、以下で定義されるデフォルトの可視性が使用され、セレクタは表示されません。" +visibilityOnBoost: "デフォルトのブースト可視性の設定" logoutConfirm: "ログアウトしますか?" lastActiveDate: "最終利用日時" statusbar: "ステータスバー" @@ -1010,6 +1021,7 @@ neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "Sharkeyを気に入っていただけましたか?" pleaseDonate: "Sharkeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!" +pleaseDonateInstance: "インスタンス管理者への寄付によって{host}を直接サポートすることもできます。" roles: "ロール" role: "ロール" noRole: "ロールはありません" @@ -1036,7 +1048,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめる" thisPostMayBeAnnoyingIgnore: "このまま投稿" -collapseRenotes: "見たことのあるリノートを省略して表示" +collapseRenotes: "見たことのあるブーストを省略して表示" collapseFiles: "ファイルを折りたたむ" autoloadConversation: "返信に会話を読み込む" internalServerError: "サーバー内部エラー" @@ -1090,7 +1102,7 @@ forceShowAds: "常に広告を表示する" addMemo: "メモを追加" editMemo: "メモを編集" reactionsList: "リアクション一覧" -renotesList: "リノート一覧" +renotesList: "ブースト一覧" notificationDisplay: "通知の表示" leftTop: "左上" rightTop: "右上" @@ -1132,9 +1144,9 @@ installed: "インストール済み" branding: "ブランディング" enableServerMachineStats: "サーバーのマシン情報を公開する" enableAchievements: "実績を有効にする" -turnOffAchievements: "これをオフにすると、達成システムは無効になります。" -enableBotTrending: "ハッシュタグにボットを追加する" -turnOffBotTrending: "これをオフにすると、ボットがハッシュタグを入力しなくなります。" +turnOffAchievements: "オフにすると実績システムは無効になります。" +enableBotTrending: "botのハッシュタグ追加を許可する" +turnOffBotTrending: "オフにするとボットがハッシュタグを入力しなくなります。" enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。" createInviteCode: "招待コードを作成" @@ -1165,7 +1177,7 @@ pastAnnouncements: "過去のお知らせ" youHaveUnreadAnnouncements: "未読のお知らせがあります。" useSecurityKey: "ブラウザまたはデバイスの指示に従って、セキュリティキーまたはパスキーを使用してください。" replies: "返信" -renotes: "リノート" +renotes: "ブースト" loadReplies: "返信を見る" loadConversation: "会話を見る" pinnedList: "ピン留めされたリスト" @@ -1178,7 +1190,7 @@ unnotifyNotes: "投稿の通知を解除" authentication: "認証" authenticationRequiredToContinue: "続けるには認証を行ってください" dateAndTime: "日時" -showRenotes: "リノートを表示" +showRenotes: "ブーストを表示" edited: "編集済み" notificationRecieveConfig: "通知の受信設定" mutualFollow: "相互フォロー" @@ -1196,6 +1208,8 @@ impressumDescription: "ドイツなどの一部の国と地域では表示が義 privacyPolicy: "プライバシーポリシー" privacyPolicyUrl: "プライバシーポリシーURL" tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" +donation: "寄付する" +donationUrl: "寄付URL" avatarDecorations: "アイコンデコレーション" attach: "付ける" detach: "外す" @@ -1219,6 +1233,20 @@ seasonalScreenEffect: "季節に応じた画面の演出" decorate: "デコる" addMfmFunction: "装飾を追加" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" +bubbleGame: "バブルゲーム" +sfx: "効果音" +soundWillBePlayed: "サウンドが再生されます" +showReplay: "リプレイを見る" +replay: "リプレイ" +replaying: "リプレイ中" +ranking: "ランキング" + +_bubbleGame: + howToPlay: "遊び方" + _howToPlay: + section1: "位置を調整してハコにモノを落とします。" + section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。" + section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -1288,8 +1316,8 @@ _initialTutorial: _visibility: description: "ノートを表示できる相手を制限できます。" public: "すべてのユーザーに公開。" - home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。" - followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。" + home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・ブーストから、他のユーザーも見ることができます。" + followers: "フォロワーにのみ公開。本人以外がブーストすることはできず、またフォロワー以外は閲覧できません。" direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。" doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。" doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。" @@ -1597,6 +1625,13 @@ _achievements: _tutorialCompleted: title: "Sharkey初心者講座 修了証" description: "チュートリアルを完了した" + _bubbleGameExplodingHead: + title: "🤯" + description: "バブルゲームで最も大きいモノを出した" + _bubbleGameDoubleExplodingHead: + title: "ダブル🤯" + description: "バブルゲームで最も大きいモノを2つ同時に出した" + flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて" _role: new: "ロールの作成" @@ -1636,7 +1671,7 @@ _role: high: "高" _options: gtlAvailable: "グローバルタイムラインの閲覧" - btlAvailable: "バブルのタイムラインを見ることができる" + btlAvailable: "バブルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" canImportNotes: "ノートのインポートが可能" @@ -1774,7 +1809,7 @@ _registry: createKey: "キーを作成" _aboutMisskey: - about: "Sharkeyは、2014年からsyuiloによって開発されているMisskeyをベースにしたオープンソースのソフトウェアです。" + about: "Sharkeyは、Misskeyをベースにしたオープンソースのソフトウェアです。" contributors: "主なコントリビューター" allContributors: "全てのコントリビューター" source: "ソースコード" @@ -1812,7 +1847,7 @@ _channel: notesCount: "{n}投稿があります" nameAndDescription: "名前と説明" nameOnly: "名前のみ" - allowRenoteToExternal: "チャンネル外へのリノートと引用リノートを許可する" + allowRenoteToExternal: "チャンネル外へのブーストと引用ブーストを許可する" _menuDisplay: sideFull: "横" @@ -1826,7 +1861,7 @@ _wordMute: muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。" _instanceMute: - instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。" + instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとブーストをミュートします。" instanceMuteDescription2: "改行で区切って設定します" title: "設定したサーバーのノートを隠します。" heading: "ミュートするサーバー" @@ -1880,7 +1915,7 @@ _theme: hashtag: "ハッシュタグ" mention: "メンション" mentionMe: "あなた宛てメンション" - renote: "Renote" + renote: "Boost" modalBg: "モーダルの背景" divider: "分割線" scrollbarHandle: "スクロールバーの取っ手" @@ -2190,13 +2225,18 @@ _profile: metadataContent: "内容" changeAvatar: "アイコン画像を変更" changeBanner: "バナー画像を変更" + updateBanner: "更新バナー" + removeBanner: "バナーを削除" changeBackground: "背景を変更する" + updateBackground: "背景を更新する" + removeBackground: "背景を削除する" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" + clips: "クリップ" followingList: "フォロー" muteList: "ミュート" blockingList: "ブロック" @@ -2316,7 +2356,7 @@ _notification: youGotMention: "{name}からのメンション" youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" - youRenoted: "{name}がRenoteしました" + youRenoted: "{name}がBoostしました" youWereFollowed: "フォローされました" youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" @@ -2331,7 +2371,7 @@ _notification: sendTestNotification: "テスト通知を送信する" notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" reactedBySomeUsers: "{n}人がリアクションしました" - renotedBySomeUsers: "{n}人がリノートしました" + renotedBySomeUsers: "{n}人がブーストしました" followedBySomeUsers: "{n}人にフォローされました" _types: @@ -2340,7 +2380,7 @@ _notification: follow: "フォロー" mention: "メンション" reply: "リプライ" - renote: "Renote" + renote: "Boost" quote: "引用" reaction: "リアクション" pollEnded: "アンケートが終了" @@ -2353,7 +2393,7 @@ _notification: _actions: followBack: "フォローバック" reply: "返信" - renote: "Renote" + renote: "Boost" _deck: alwaysShowMainColumn: "常にメインカラムを表示" @@ -2411,7 +2451,7 @@ _webhookSettings: followed: "フォローされたとき" note: "ノートを投稿したとき" reply: "返信されたとき" - renote: "Renoteされたとき" + renote: "Boostされたとき" reaction: "リアクションがあったとき" mention: "メンションされたとき" @@ -2508,15 +2548,15 @@ _animatedMFM: play: "MFMアニメーションを再生" stop: "MFMアニメーション停止" _alert: - text: "アニメーションMFMには、点滅するライトや高速で動くテキスト/絵文字を含めることができる。" - confirm: "アニメイト" + text: "MFMアニメーションには、点滅するライトや高速で動くテキスト/絵文字を含まれる場合があります。" + confirm: "再生する" _dataRequest: - title: "リクエストデータ" - warn: "データのリクエストは3日ごとにしかできない。" - text: "データのダウンロードが完了すると、このアカウントに登録されているEメールアドレスにEメールが送信されます。" + title: "データリクエスト" + warn: "データリクエストは3日ごとに可能です。" + text: "データの保存が完了すると、このアカウントに登録されているEメールアドレスにメールが送信されます。" button: "リクエスト" - + _dataSaver: _media: title: "メディアの読み込み" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 1a78c1ec4..1f9795a5d 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -15,7 +15,7 @@ gotIt: "ほい" cancel: "やめとく" noThankYou: "やめとく" enterUsername: "ユーザー名を入れてや" -renotedBy: "{user}がリノートしたで" +renotedBy: "{user}がブーストしたで" noNotes: "ノートはあらへん" noNotifications: "通知はあらへん" instance: "サーバー" @@ -45,10 +45,10 @@ pin: "ピン留めしとく" unpin: "やっぱピン留めせん" copyContent: "内容をコピー" copyLink: "リンクをコピー" -copyLinkRenote: "リノートのリンクをコピーするで?" +copyLinkRenote: "ブーストのリンクをコピーするで?" delete: "ほかす" deleteAndEdit: "ほかして直す" -deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、リノート、返信も全部消えるんやけどそれでもええん?" +deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、ブースト、返信も全部消えるんやけどそれでもええん?" addToList: "リストに入れたる" addToAntenna: "アンテナに入れる" sendMessage: "メッセージを送る" @@ -105,13 +105,13 @@ followRequests: "フォロー申請" unfollow: "フォローやめる" followRequestPending: "フォロー許してくれるん待っとる" enterEmoji: "絵文字を入れてや" -renote: "リノート" -unrenote: "リノートやめる" -renoted: "リノートしたで。" -cantRenote: "この投稿はリノートできへんっぽい。" -cantReRenote: "リノート自体はリノートできへんで。" +renote: "ブースト" +unrenote: "ブーストやめる" +renoted: "ブーストしたで。" +cantRenote: "この投稿はブーストできへんっぽい。" +cantReRenote: "ブースト自体はブーストできへんで。" quote: "引用" -inChannelRenote: "チャンネルの中でリノート" +inChannelRenote: "チャンネルの中でブースト" inChannelQuote: "チャンネル内引用" pinnedNote: "ピン留めされとるノート" pinned: "ピン留めしとく" @@ -135,8 +135,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ" enterFileName: "ファイル名を入れてや" mute: "ミュート" unmute: "ミュートやめたる" -renoteMute: "リノートは見いひん" -renoteUnmute: "リノートもやっぱ見るわ" +renoteMute: "ブーストは見いひん" +renoteUnmute: "ブーストもやっぱ見るわ" block: "ブロック" unblock: "ブロックやめたる" suspend: "凍結" @@ -677,7 +677,7 @@ behavior: "動作" sample: "サンプル" abuseReports: "通報" reportAbuse: "通報" -reportAbuseRenote: "リノート苦情だすで?" +reportAbuseRenote: "ブースト苦情だすで?" reportAbuseOf: "{name}を通報する" fillAbuseReportDescription: "細かい通報理由を書いてなー。対象ノートがある時はそのURLも書いといてなー。" abuseReported: "無事内容が送信されたみたいやで。おおきに〜。" @@ -711,9 +711,9 @@ manageAccessTokens: "アクセストークンの管理" accountInfo: "アカウント情報" notesCount: "ノートの数やで" repliesCount: "返信した数やで" -renotesCount: "リノートした数やで" +renotesCount: "ブーストした数やで" repliedCount: "返信された数やで" -renotedCount: "リノートされた数やで" +renotedCount: "ブーストされた数やで" followingCount: "フォロー数やで" followersCount: "フォロワー数やで" sentReactionsCount: "ツッコんだ数" @@ -1009,7 +1009,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。" thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめとく" thisPostMayBeAnnoyingIgnore: "このまま投稿" -collapseRenotes: "見たことあるリノートは飛ばして表示するで" +collapseRenotes: "見たことあるブーストは飛ばして表示するで" internalServerError: "サーバー内部エラー" internalServerErrorDescription: "サーバーでなんか変なこと起こっとるわ。" copyErrorInfo: "エラー情報をコピるで" @@ -1060,7 +1060,7 @@ forceShowAds: "いっつも広告を映す" addMemo: "メモを足す" editMemo: "メモをいらう" reactionsList: "ツッコミ一覧" -renotesList: "リノート一覧" +renotesList: "ブースト一覧" notificationDisplay: "通知見せる" leftTop: "左上" rightTop: "右上" @@ -1131,7 +1131,7 @@ pastAnnouncements: "過去のお知らせやで" youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんやろ。" useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。" replies: "返事" -renotes: "リノート" +renotes: "ブースト" loadReplies: "返信を見るで" loadConversation: "会話を見るで" pinnedList: "ピン留めしはったリスト" @@ -1142,7 +1142,7 @@ unnotifyNotes: "投稿の通知やめる" authentication: "認証" authenticationRequiredToContinue: "続けるんなら認証してや。" dateAndTime: "日時" -showRenotes: "リノート出す" +showRenotes: "ブースト出す" edited: "いじったやつ" notificationRecieveConfig: "通知もらうかの設定" mutualFollow: "お互いフォローしてんで" @@ -1247,8 +1247,8 @@ _initialTutorial: _visibility: description: "ノートを見れる相手を制限できるわ。" public: "みんなに見せるで。" - home: "ホームタイムラインにだけ見せるで。フォロワーとか、プロフィールを見に来た人、リノートからも見れるから、実質は全員見れるけどな。あんまし広がりにくいってことや。" - followers: "フォロワーにだけ見せるで。自分以外はリノートできへんし、フォロワー以外は絶対に見れへん。" + home: "ホームタイムラインにだけ見せるで。フォロワーとか、プロフィールを見に来た人、ブーストからも見れるから、実質は全員見れるけどな。あんまし広がりにくいってことや。" + followers: "フォロワーにだけ見せるで。自分以外はブーストできへんし、フォロワー以外は絶対に見れへん。" direct: "指定した人にだけ公開されて、ついでに通知も送るで。ダイレクトメールの代わりとして使ってな。" doNotSendConfidencialOnDirect1: "機密情報を送るときは十分注意せえよ。" doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容が見れるから、信用できへんサーバーのひとにダイレクト投稿するときには、めっちゃ用心しとくんやで。" @@ -1709,7 +1709,7 @@ _registry: domain: "ドメイン" createKey: "キーを作る" _aboutMisskey: - about: "Sharkeyは、syuiloが2014年からずっと作ってはる、Misskeyをベースにしたオープンソースなソフトウェアや。" + about: "Sharkeyは、Misskeyをベースにしたオープンソースなソフトウェアや。" contributors: "主な貢献者" allContributors: "全ての貢献者" source: "ソースコード" @@ -1742,7 +1742,7 @@ _channel: notesCount: "{n}こ投稿があるで" nameAndDescription: "名前と説明" nameOnly: "名前だけ" - allowRenoteToExternal: "チャンネルの外にリノートできるようにする" + allowRenoteToExternal: "チャンネルの外にブーストできるようにする" _menuDisplay: sideFull: "横" sideIcon: "横(アイコン)" @@ -2164,7 +2164,7 @@ _notification: youGotMention: "{name}からのメンション" youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" - youRenoted: "{name}がリノートしたみたいやで" + youRenoted: "{name}がブーストしたみたいやで" youWereFollowed: "フォローされたで" youReceivedFollowRequest: "フォロー許可してほしいみたいやな" yourFollowRequestAccepted: "フォローさせてもろたで" @@ -2178,7 +2178,7 @@ _notification: sendTestNotification: "テスト通知を送信するで" notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで" reactedBySomeUsers: "{n}人がツッコんだで" - renotedBySomeUsers: "{n}人がリノートしたで" + renotedBySomeUsers: "{n}人がブーストしたで" followedBySomeUsers: "{n}人にフォローされたで" _types: all: "すべて" @@ -2249,7 +2249,7 @@ _webhookSettings: followed: "フォローもらったとき~!" note: "ノートを投稿したとき~!" reply: "返信があるとき~!" - renote: "リノートされるとき~!" + renote: "ブーストされるとき~!" reaction: "ツッコまれたとき~!" mention: "メンションがあるとき~!" _moderationLogTypes: diff --git a/package.json b/package.json index ec9e98175..6ed8726cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2023.12.0.beta3", + "version": "2024.1.0.beta2", "codename": "shonk", "repository": { "type": "git", diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 97d777c86..5a4aa4e15 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -160,7 +160,6 @@ 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 new file mode 100644 index 000000000..4502da47d --- /dev/null +++ b/packages/backend/jest.config.e2e.cjs @@ -0,0 +1,15 @@ +/* +* 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 new file mode 100644 index 000000000..aa5992936 --- /dev/null +++ b/packages/backend/jest.config.unit.cjs @@ -0,0 +1,14 @@ +/* +* 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/1703658526000-supportTrueMailApi.js b/packages/backend/migration/1703658526000-supportTrueMailApi.js new file mode 100644 index 000000000..0054d5412 --- /dev/null +++ b/packages/backend/migration/1703658526000-supportTrueMailApi.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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/1704373210054-support-mcaptcha.js b/packages/backend/migration/1704373210054-support-mcaptcha.js new file mode 100644 index 000000000..ce42b9071 --- /dev/null +++ b/packages/backend/migration/1704373210054-support-mcaptcha.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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/1704744370000-add-donation-url.js b/packages/backend/migration/1704744370000-add-donation-url.js new file mode 100644 index 000000000..c953b13cc --- /dev/null +++ b/packages/backend/migration/1704744370000-add-donation-url.js @@ -0,0 +1,10 @@ +export class AddDonationUrl1704744370000 { + name = 'AddDonationUrl1704744370000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "donationUrl" character varying(1024)`); + } + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "donationUrl"`); + } +} diff --git a/packages/backend/migration/1704959805077-bubble-game-record.js b/packages/backend/migration/1704959805077-bubble-game-record.js new file mode 100644 index 000000000..cc45b09c8 --- /dev/null +++ b/packages/backend/migration/1704959805077-bubble-game-record.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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/package.json b/packages/backend/package.json index 2aa10b1c9..719c3e98d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -13,6 +13,7 @@ "revert": "pnpm typeorm migration:revert -d ormconfig.js", "check:connect": "node ./check_connect.js", "build": "swc src -d built -D", + "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc", "watch:swc": "swc src -d built -D -w", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", @@ -21,11 +22,15 @@ "typecheck": "pnpm --filter megalodon build && tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.ts\"", "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": "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", "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 ./generate_api_json.js" }, "optionalDependencies": { @@ -72,11 +77,13 @@ "@fastify/multipart": "8.0.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", + "@misskey-dev/sharp-read-bmp": "^1.1.1", + "@misskey-dev/summaly": "^5.0.3", "@nestjs/common": "10.2.10", "@nestjs/core": "10.2.10", "@nestjs/testing": "10.2.10", "@peertube/http-signature": "1.7.0", - "@sharkey/sfm-js": "0.24.3", + "@sharkey/sfm-js": "0.24.4", "@simplewebauthn/server": "8.3.5", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.1.10", @@ -158,11 +165,9 @@ "sanitize-html": "2.11.0", "secure-json-parse": "2.7.0", "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", - "summaly": "github:misskey-dev/summaly", "systeminformation": "5.21.20", "tinycolor2": "1.6.0", "tmp": "0.2.1", @@ -179,6 +184,8 @@ }, "devDependencies": { "@jest/globals": "29.7.0", + "@misskey-dev/eslint-plugin": "^1.0.0", + "@nestjs/platform-express": "^10.3.0", "@simplewebauthn/typescript-types": "8.3.4", "@swc/jest": "0.2.29", "@types/accepts": "1.3.7", @@ -228,9 +235,11 @@ "eslint": "8.56.0", "eslint-plugin-import": "2.29.1", "execa": "8.0.1", + "fkill": "^9.0.0", "jest": "29.7.0", "jest-mock": "29.7.0", "nodemon": "3.0.2", + "pid-port": "^1.0.0", "simple-oauth2": "5.0.0" } } diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 3e9d19f82..c83845b94 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { setTimeout } from 'node:timers/promises'; import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; @@ -12,6 +11,7 @@ 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 = { @@ -33,7 +33,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 { @@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, - ) {} + ) { } public async dispose(): Promise { - 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); - } + // Wait for all potential DB queries + await allSettled(); + // And then disconnect from DB await Promise.all([ this.db.destroy(), this.redisClient.disconnect(), diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 24a0296aa..a550fdc36 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -7,8 +7,8 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; -import type { RedisOptions } from 'ioredis'; import { globSync } from 'glob'; +import type { RedisOptions } from 'ioredis'; type RedisOptionsSource = Partial & { host: string; @@ -65,6 +65,7 @@ type Source = { allowedPrivateNetworks?: string[]; maxFileSize?: number; + maxNoteLength?: number; clusterLimit?: number; @@ -133,6 +134,7 @@ export type Config = { proxyBypassHosts: string[] | undefined; allowedPrivateNetworks: string[] | undefined; maxFileSize: number | undefined; + maxNoteLength: number; clusterLimit: number | undefined; id: string; outgoingAddress: string | undefined; @@ -199,7 +201,7 @@ export function loadConfig(): Config { JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; - const config = globSync(path) + const config = globSync(path).sort() .map(path => fs.readFileSync(path, 'utf-8')) .map(contents => yaml.load(contents) as Source) .reduce( @@ -249,6 +251,7 @@ export function loadConfig(): Config { proxyBypassHosts: config.proxyBypassHosts, allowedPrivateNetworks: config.allowedPrivateNetworks, maxFileSize: config.maxFileSize, + maxNoteLength: config.maxNoteLength ?? 3000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, outgoingAddressFamily: config.outgoingAddressFamily, diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 88fc03385..a28b68ee8 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [ 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', + 'bubbleGameExplodingHead', + 'bubbleGameDoubleExplodingHead', ] as const; @Injectable() diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index f64196f4f..6c5ee4835 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -73,6 +73,37 @@ 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/DriveService.ts b/packages/backend/src/core/DriveService.ts index 9b6187be4..fc1927bfa 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -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 'sharp-read-bmp'; +import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import { IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; @@ -634,7 +634,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 && !this.driveFileEntityService.validateFileName(file.name)) { + if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) { throw new DriveService.InvalidFileNameError(); } diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 7fc780078..7e812b4df 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -156,7 +156,7 @@ export class EmailService { @bindThis public async validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; - reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned'; + reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; }> { const meta = await this.metaService.fetch(); @@ -173,6 +173,8 @@ export class EmailService { 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, @@ -201,6 +203,8 @@ export class EmailService { validated.reason === 'disposable' ? 'disposable' : validated.reason === 'mx' ? 'mx' : validated.reason === 'smtp' ? 'smtp' : + validated.reason === 'network' ? 'network' : + validated.reason === 'blacklist' ? 'blacklist' : null, }; } @@ -265,4 +269,67 @@ 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; + errors?: { + list_match?: string; + regex?: string; + mx?: string; + smtp?: string; + } | null; + }; + + if (json.email === undefined || (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/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 3bc4a29b9..58f3d3559 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -58,6 +58,7 @@ 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'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -862,7 +863,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.relayService.deliverToRelays(user, noteActivity); } - dm.execute(); + trackPromise(dm.execute()); })(); } //#endregion diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index 03c1735e0..c73cf7659 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -14,6 +14,7 @@ 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 { @@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown { // TODO: ↓まとめてクエリしたい - this.noteUnreadsRepository.countBy({ + trackPromise(this.noteUnreadsRepository.countBy({ userId: userId, isMentioned: true, }).then(mentionsCount => { @@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown { // 全て既読になったイベントを発行 this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); } - }); + })); - this.noteUnreadsRepository.countBy({ + trackPromise(this.noteUnreadsRepository.countBy({ userId: userId, isSpecified: true, }).then(specifiedCount => { @@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown { // 全て既読になったイベントを発行 this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); } - }); + })); } } diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index ad7be83e5..765fcae06 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -20,6 +20,7 @@ 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 { @@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - public async createNotification( + 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( notifieeId: MiUser['id'], type: T, data: Omit, 'type' | 'id' | 'createdAt' | 'notifierId'>, diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 4444dc978..20a53ff28 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -3,12 +3,12 @@ * 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 { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { QUEUE, baseQueueOptions } from '@/queue/const.js'; +import { allSettled } from '@/misc/promise-tracker.js'; import type { Provider } from '@nestjs/common'; import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; @@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown { ) {} public async dispose(): Promise { - 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); - } + // Wait for all potential queue jobs + await allSettled(); + // And then close all queues await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 2ee61eb54..e1c84535a 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -17,6 +17,7 @@ import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '. import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; import { MiNote } from '@/models/Note.js'; +import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; @Injectable() export class QueueService { @@ -75,11 +76,15 @@ 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, + content: contentBody, + digest, to, isSharedInbox, }; @@ -104,6 +109,8 @@ 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, @@ -118,7 +125,8 @@ export class QueueService { name: d[0], data: { user, - content, + content: contentBody, + digest, to: d[0], isSharedInbox: d[1], } as DeliverJobData, @@ -185,6 +193,16 @@ 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', { diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 0daee34ce..11c972982 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -28,6 +28,7 @@ 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'; const FALLBACK = '❤'; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; @@ -280,7 +281,7 @@ export class ReactionService { } } - dm.execute(); + trackPromise(dm.execute()); } //#endregion } @@ -328,7 +329,7 @@ export class ReactionService { dm.addDirectRecipe(reactee as MiRemoteUser); } dm.addFollowersRecipe(); - dm.execute(); + trackPromise(dm.execute()); } //#endregion } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 81003bcf1..d7414e9c9 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -144,7 +144,7 @@ class DeliverManager { } // deliver - this.queueService.deliverMany(this.actor, this.activity, inboxes); + await 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 d8616d293..3f01c0289 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -97,6 +97,8 @@ export class ApInboxService { } catch (err) { if (err instanceof Error || typeof err === 'string') { this.logger.error(err); + } else { + throw err; } } } @@ -256,7 +258,7 @@ export class ApInboxService { const targetUri = getApId(activity.object); - this.announceNote(actor, activity, targetUri); + await this.announceNote(actor, activity, targetUri); } @bindThis @@ -288,7 +290,7 @@ export class ApInboxService { } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { - if (err.isClientError) { + if (!err.isRetryable) { this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`); return; } @@ -373,7 +375,7 @@ export class ApInboxService { }); if (isPost(object)) { - this.createNote(resolver, actor, object, false, activity); + await this.createNote(resolver, actor, object, false, activity); } else { this.logger.warn(`Unknown type: ${getApType(object)}`); } @@ -404,7 +406,7 @@ export class ApInboxService { await this.apNoteService.createNote(note, resolver, silent); return 'ok'; } catch (err) { - if (err instanceof StatusError && err.isClientError) { + if (err instanceof StatusError && !err.isRetryable) { return `skip ${err.statusCode}`; } else { throw err; diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index b59ce5241..e165c5e96 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -34,9 +34,9 @@ type PrivateKey = { }; export class ApRequestCreator { - static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }): Signed { + static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record }): Signed { const u = new URL(args.url); - const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; + const digestHeader = args.digest ?? this.createDigest(args.body); const request: Request = { url: u.href, @@ -59,6 +59,10 @@ 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); @@ -145,8 +149,8 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise { - const body = JSON.stringify(object); + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise { + const body = typeof object === 'string' ? object : JSON.stringify(object); const keypair = await this.userKeypairService.getUserKeypair(user.id); @@ -157,6 +161,7 @@ export class ApRequestService { }, url, body, + digest, additionalHeaders: { }, }); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 2595783e0..12958811e 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -221,7 +221,7 @@ export class ApNoteService { return { status: 'ok', res }; } catch (e) { return { - status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', + status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; } }; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index fee96bb80..a5d305446 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -369,6 +369,7 @@ export class NoteEntityService implements OnModuleInit { color: channel.color, isSensitive: channel.isSensitive, allowRenoteToExternal: channel.allowRenoteToExternal, + userId: channel.userId, } : undefined, mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined, uri: note.uri ?? undefined, diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index c5ef9b2fa..4c55acea5 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -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 ?? 50)); + ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length)); }); const tick = async () => { diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 0c5ac8f2d..b6327e5db 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -79,5 +79,6 @@ export const DI = { flashLikesRepository: Symbol('flashLikesRepository'), userMemosRepository: Symbol('userMemosRepository'), noteEditRepository: Symbol('noteEditRepository'), + bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), //#endregion }; diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index 5c10559ec..0a19036c9 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -71,8 +71,11 @@ export default class Logger { let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; - console.log(important ? chalk.bold(log) : log); - if (level === 'error' && data) console.log(data); + const args: unknown[] = [important ? chalk.bold(log) : log]; + if (data != null) { + args.push(data); + } + console.log(...args); } @bindThis diff --git a/packages/backend/src/misc/promise-tracker.ts b/packages/backend/src/misc/promise-tracker.ts new file mode 100644 index 000000000..c7166c6de --- /dev/null +++ b/packages/backend/src/misc/promise-tracker.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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/status-error.ts b/packages/backend/src/misc/status-error.ts index 4285685d2..be213088a 100644 --- a/packages/backend/src/misc/status-error.ts +++ b/packages/backend/src/misc/status-error.ts @@ -7,6 +7,7 @@ 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); @@ -14,5 +15,6 @@ 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/models/BubbleGameRecord.ts b/packages/backend/src/models/BubbleGameRecord.ts new file mode 100644 index 000000000..4b483ed4d --- /dev/null +++ b/packages/backend/src/models/BubbleGameRecord.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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: any[]; + + @Column('boolean', { + default: false, + }) + public isVerified: boolean; +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 4bf856e61..6d5c4b374 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -196,6 +196,29 @@ 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, }) @@ -362,6 +385,12 @@ export class MiMeta { }) public privacyPolicyUrl: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public donationUrl: string | null; + @Column('varchar', { length: 8192, nullable: true, @@ -467,6 +496,23 @@ 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, }) diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 0b5d3b640..9c42b3174 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -5,7 +5,7 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, 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, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit } from './_.js'; +import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, 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, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit, MiBubbleGameRecord } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -405,6 +405,12 @@ const $noteEditRepository: Provider = { inject: [DI.db], }; +const $bubbleGameRecordsRepository: Provider = { + provide: DI.bubbleGameRecordsRepository, + useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord), + inject: [DI.db], +}; + @Module({ imports: [ ], @@ -475,6 +481,7 @@ const $noteEditRepository: Provider = { $flashLikesRepository, $userMemosRepository, $noteEditRepository, + $bubbleGameRecordsRepository, ], exports: [ $usersRepository, @@ -543,6 +550,7 @@ const $noteEditRepository: Provider = { $flashLikesRepository, $userMemosRepository, $noteEditRepository, + $bubbleGameRecordsRepository, ], }) export class RepositoryModule {} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 2a7810235..53b9f44cc 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -69,6 +69,7 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { NoteEdit } from '@/models/NoteEdit.js'; +import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import type { Repository } from 'typeorm'; export { @@ -138,6 +139,7 @@ export { MiFlashLike, MiUserMemo, NoteEdit, + MiBubbleGameRecord, }; export type AbuseUserReportsRepository = Repository; @@ -206,3 +208,4 @@ export type FlashsRepository = Repository; export type FlashLikesRepository = Repository; export type UserMemoRepository = Repository; export type NoteEditRepository = Repository; +export type BubbleGameRecordsRepository = Repository; diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index aa749943f..2b7722129 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -148,6 +148,10 @@ export const packedNoteSchema = { type: 'boolean', optional: false, nullable: false, }, + userId: { + type: 'string', + optional: false, nullable: true, + }, }, }, localOnly: { diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 18773a1b6..395c7ab44 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -77,6 +77,7 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { NoteEdit } from '@/models/NoteEdit.js'; +import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -192,6 +193,7 @@ export const entities = [ MiFlashLike, MiUserMemo, NoteEdit, + MiBubbleGameRecord, ...charts, ]; diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 29dc78605..d547a498a 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -25,6 +25,7 @@ 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'; @@ -56,6 +57,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor ExportAccountDataProcessorService, ExportCustomEmojisProcessorService, ExportNotesProcessorService, + ExportClipsProcessorService, ExportFavoritesProcessorService, ExportFollowingProcessorService, ExportMutingProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index ea3ecd4de..cdca744b8 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -17,6 +17,7 @@ import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesP import { ExportAccountDataProcessorService } from './processors/ExportAccountDataProcessorService.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'; @@ -94,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private exportAccountDataProcessorService: ExportAccountDataProcessorService, private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, private exportNotesProcessorService: ExportNotesProcessorService, + private exportClipsProcessorService: ExportClipsProcessorService, private exportFavoritesProcessorService: ExportFavoritesProcessorService, private exportFollowingProcessorService: ExportFollowingProcessorService, private exportMutingProcessorService: ExportMutingProcessorService, @@ -169,6 +171,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'exportAccountData': return this.exportAccountDataProcessorService.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); diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 4a1d9f28b..64c344555 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -72,7 +72,7 @@ export class DeliverProcessorService { } try { - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content); + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); // Update stats this.federatedInstanceService.fetch(host).then(i => { @@ -111,7 +111,7 @@ export class DeliverProcessorService { if (res instanceof StatusError) { // 4xx - if (res.isClientError) { + if (!res.isRetryable) { // 相手が閉鎖していることを明示しているため、配送停止する if (job.data.isSharedInbox && res.statusCode === 410) { this.federatedInstanceService.fetch(host).then(i => { diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts new file mode 100644 index 000000000..5221497bd --- /dev/null +++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as fs from 'node:fs'; +import { Writable } from 'node:stream'; +import { Inject, Injectable, StreamableFile } 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, NotesRepository, 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 { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { Packed } from '@/misc/json-schema.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/ImportNotesProcessorService.ts b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts index 552b69d92..03a0e951b 100644 --- a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts @@ -1,5 +1,6 @@ import * as fs from 'node:fs'; import * as vm from 'node:vm'; +import * as crypto from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { ZipReader } from 'slacc'; import { DI } from '@/di-symbols.js'; @@ -72,7 +73,6 @@ export class ImportNotesProcessorService { } } - // Function was taken from Firefish and modified for our needs @bindThis private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise { type NotesMap = { @@ -378,7 +378,11 @@ export class ImportNotesProcessorService { return; } - if (toot.directMessage) return; + const followers = toot.to.some((str: string) => str.includes('/followers')); + + if (toot.directMessage || !toot.to.includes('https://www.w3.org/ns/activitystreams#Public') && !followers) return; + + const visibility = followers ? toot.cc.includes('https://www.w3.org/ns/activitystreams#Public') ? 'home' : 'followers' : 'public'; const date = new Date(toot.object.published); let text = undefined; @@ -417,7 +421,7 @@ export class ImportNotesProcessorService { } } - const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply }); + const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, visibility: visibility, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply }); if (toot.childNotes) this.queueService.createImportMastoToDbJob(user, toot.childNotes, createdNote.id); } @@ -469,7 +473,9 @@ export class ImportNotesProcessorService { for await (const file of post.object.attachment) { const slashdex = file.url.lastIndexOf('/'); - const name = file.url.substring(slashdex + 1); + const filename = file.url.substring(slashdex + 1); + const hash = crypto.createHash('md5').update(file.url).digest('base64url'); + const name = `${hash}-${filename}`; const [filePath, cleanup] = await createTemp(); const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: pleroFolder?.id }); @@ -484,6 +490,7 @@ export class ImportNotesProcessorService { user: user, path: filePath, name: name, + comment: file.name, folderId: pleroFolder?.id, }); files.push(driveFile); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index f69634968..971e9f497 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -85,7 +85,7 @@ export class InboxProcessorService { } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { - if (err.isClientError) { + if (!err.isRetryable) { 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}`); diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index a41f5565c..7a0d53384 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -71,7 +71,7 @@ export class WebhookDeliverProcessorService { if (res instanceof StatusError) { // 4xx - if (res.isClientError) { + if (!res.isRetryable) { 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 432b3d364..372829a82 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -15,7 +15,9 @@ export type DeliverJobData = { /** Actor */ user: ThinUser; /** Activity */ - content: unknown; + content: string; + /** Digest header */ + digest: string; /** inbox URL to deliver */ to: string; /** whether it is sharedInbox */ diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 68de73823..24f33fd60 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -129,6 +129,13 @@ export class ActivityPubServerService { this is also inspired by FireFish's `checkFetch` */ + /* tell any caching proxy that they should not cache these + responses: we wouldn't want the proxy to return a 403 to + someone presenting a valid signature, or return a cached + response body to someone we've blocked! + */ + reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); + /* we always allow requests about our instance actor, because when a remote instance needs to check our signature on a request we sent, it will need to fetch information about the user that @@ -155,23 +162,25 @@ export class ActivityPubServerService { return true; } + const keyId = new URL(signature.keyId); + const keyHost = this.utilityService.toPuny(keyId.hostname); + + const logPrefix = `${request.id} ${request.url} (by ${request.headers['user-agent']}) apparently from ${keyHost}:`; + if (signature.params.headers.indexOf('host') === -1 || request.headers.host !== this.config.host) { // no destination host, or not us: refuse - this.authlogger.warn(`${request.id} ${request.url} no destination host, or not us: refuse`); + this.authlogger.warn(`${logPrefix} no destination host, or not us: refuse`); reply.code(401); return true; } - const keyId = new URL(signature.keyId); - const keyHost = this.utilityService.toPuny(keyId.hostname); - const meta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(meta.blockedHosts, keyHost)) { /* blocked instance: refuse (we don't care if the signature is good, if they even pretend to be from a blocked instance, they're out) */ - this.authlogger.warn(`${request.id} ${request.url} instance ${keyHost} is blocked: refuse`); + this.authlogger.warn(`${logPrefix} instance is blocked: refuse`); reply.code(401); return true; } @@ -186,13 +195,13 @@ export class ActivityPubServerService { /* keyId is often in the shape `${user.uri}#${keyname}`, try fetching information about the remote user */ const candidate = formatURL(keyId, { fragment: false }); - this.authlogger.info(`${request.id} ${request.url} we don't know the user for keyId ${keyId}, trying to fetch via ${candidate}`); + this.authlogger.info(`${logPrefix} we don't know the user for keyId ${keyId}, trying to fetch via ${candidate}`); authUser = await this.apDbResolverService.getAuthUserFromApId(candidate); } if (authUser?.key == null) { // we can't figure out who the signer is, or we can't get their key: refuse - this.authlogger.warn(`${request.id} ${request.url} we can't figure out who the signer is, or we can't get their key: refuse`); + this.authlogger.warn(`${logPrefix} we can't figure out who the signer is, or we can't get their key: refuse`); reply.code(401); return true; } @@ -200,20 +209,20 @@ export class ActivityPubServerService { let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); if (!httpSignatureValidated) { - this.authlogger.info(`${request.id} ${request.url} failed to validate signature, re-fetching the key for ${authUser.user.uri}`); + this.authlogger.info(`${logPrefix} failed to validate signature, re-fetching the key for ${authUser.user.uri}`); // maybe they changed their key? refetch it authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user); if (authUser.key != null) { httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); } else { - this.authlogger.warn(`${request.id} ${request.url} failed to re-fetch key for ${authUser.user}`); + this.authlogger.warn(`${logPrefix} failed to re-fetch key for ${authUser.user}`); } } if (!httpSignatureValidated) { // bad signature: refuse - this.authlogger.info(`${request.id} ${request.url} failed to validate signature: refuse`); + this.authlogger.info(`${logPrefix} failed to validate signature: refuse`); reply.code(401); return true; } @@ -322,11 +331,11 @@ export class ActivityPubServerService { if (profile.followersVisibility === 'private') { reply.code(403); - reply.header('Cache-Control', 'public, max-age=30'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } else if (profile.followersVisibility === 'followers') { reply.code(403); - reply.header('Cache-Control', 'public, max-age=30'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } //#endregion @@ -378,7 +387,7 @@ export class ActivityPubServerService { user.followersCount, `${partOf}?page=true`, ); - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -416,11 +425,11 @@ export class ActivityPubServerService { if (profile.followingVisibility === 'private') { reply.code(403); - reply.header('Cache-Control', 'public, max-age=30'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } else if (profile.followingVisibility === 'followers') { reply.code(403); - reply.header('Cache-Control', 'public, max-age=30'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } //#endregion @@ -472,7 +481,7 @@ export class ActivityPubServerService { user.followingCount, `${partOf}?page=true`, ); - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -513,7 +522,7 @@ export class ActivityPubServerService { renderedNotes, ); - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -604,7 +613,7 @@ export class ActivityPubServerService { `${partOf}?page=true`, `${partOf}?page=true&since_id=000000000000000000000000`, ); - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -617,7 +626,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); } @@ -707,7 +716,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false)); }); @@ -730,7 +739,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.packActivity(note))); }); @@ -775,7 +784,7 @@ export class ActivityPubServerService { const keypair = await this.userKeypairService.getUserKeypair(user.id); if (this.userEntityService.isLocalUser(user)) { - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair))); } else { @@ -825,7 +834,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji))); }); @@ -848,7 +857,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note))); }); @@ -876,7 +885,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); @@ -913,7 +922,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index e82ef64dc..61e8e8c84 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -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 'sharp-read-bmp'; +import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import type { Config } from '@/config.js'; import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 37a120a6f..a3e3c39ec 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MemorySingleCache } from '@/misc/cache.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -109,6 +108,7 @@ export class NodeinfoServerService { tosUrl: meta.termsOfServiceUrl, privacyPolicyUrl: meta.privacyPolicyUrl, impressumUrl: meta.impressumUrl, + donationUrl: meta.donationUrl, repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, disableRegistration: meta.disableRegistration, @@ -118,7 +118,7 @@ export class NodeinfoServerService { emailRequiredForSignup: meta.emailRequiredForSignup, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, + maxNoteTextLength: this.config.maxNoteLength, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, proxyAccountName: proxyAccount ? proxyAccount.username : null, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index ed1b2d437..1f37c74c0 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -214,6 +214,7 @@ 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'; @@ -376,6 +377,8 @@ import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js'; import * as ep___retention from './endpoints/retention.js'; import * as ep___sponsors from './endpoints/sponsors.js'; +import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js'; +import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -588,6 +591,7 @@ 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 }; @@ -750,6 +754,8 @@ const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.d const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default }; const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.default }; +const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default }; +const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default }; @Module({ imports: [ @@ -966,6 +972,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, @@ -1128,6 +1135,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $fetchExternalResources, $retention, $sponsors, + $bubbleGame_register, + $bubbleGame_ranking, ], exports: [ $admin_meta, @@ -1338,6 +1347,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $i_exportFollowing, $i_exportMute, $i_exportNotes, + $i_exportClips, $i_exportFavorites, $i_exportUserLists, $i_exportAntennas, @@ -1497,6 +1507,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $fetchExternalResources, $retention, $sponsors, + $bubbleGame_register, + $bubbleGame_ranking, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 63379c887..8788a1fd6 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -70,6 +70,7 @@ export class SignupApiService { 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; + 'm-captcha-response'?: string; } }>, reply: FastifyReply, @@ -87,6 +88,12 @@ 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); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index f82bf257f..7510228bb 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Schema } from '@/misc/json-schema.js'; import { permissions } from 'misskey-js'; +import type { Schema } from '@/misc/json-schema.js'; import { RolePolicies } from '@/core/RoleService.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; @@ -215,6 +215,7 @@ 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'; @@ -377,6 +378,8 @@ import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js'; import * as ep___retention from './endpoints/retention.js'; import * as ep___sponsors from './endpoints/sponsors.js'; +import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js'; +import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -587,6 +590,7 @@ 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], @@ -749,6 +753,8 @@ const eps = [ ['fetch-external-resources', ep___fetchExternalResources], ['retention', ep___retention], ['sponsors', ep___sponsors], + ['bubble-game/register', ep___bubbleGame_register], + ['bubble-game/ranking', ep___bubbleGame_ranking], ]; interface IEndpointMetaBase { 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 f3e0c1ef1..be4d16589 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 @@ -98,11 +98,12 @@ export default class extends Endpoint { // eslint- } if (ps.query) { - q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); + q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }) + .orderBy('length(emoji.name)', 'ASC'); } const emojis = await q - .orderBy('emoji.id', 'DESC') + .addOrderBy('emoji.id', 'DESC') .limit(ps.limit) .getMany(); 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 59e87253f..26b70587a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -91,7 +91,7 @@ export default class extends Endpoint { // eslint- //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); //const emojis = await q.limit(ps.limit).getMany(); - emojis = await q.getMany(); + emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany(); const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g); if (queryarry) { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 4fd2a568a..9fe997f88 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -45,6 +45,18 @@ 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, @@ -174,6 +186,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + mcaptchaSecretKey: { + type: 'string', + optional: false, nullable: true, + }, recaptchaSecretKey: { type: 'string', optional: false, nullable: true, @@ -299,6 +315,18 @@ 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, @@ -387,6 +415,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + donationUrl: { + type: 'string', + optional: false, nullable: true, + }, maintainerEmail: { type: 'string', optional: false, nullable: true, @@ -470,12 +502,16 @@ export default class extends Endpoint { // eslint- repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, impressumUrl: instance.impressumUrl, + donationUrl: instance.donationUrl, privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, approvalRequiredForSignup: instance.approvalRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableMcaptcha: instance.enableMcaptcha, + mcaptchaSiteKey: instance.mcaptchaSitekey, + mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, enableRecaptcha: instance.enableRecaptcha, recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, @@ -508,6 +544,7 @@ export default class extends Endpoint { // eslint- preservedUsernames: instance.preservedUsernames, bubbleInstances: instance.bubbleInstances, hcaptchaSecretKey: instance.hcaptchaSecretKey, + mcaptchaSecretKey: instance.mcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, sensitiveMediaDetection: instance.sensitiveMediaDetection, @@ -543,6 +580,9 @@ 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, 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 5c916fe34..786a628d6 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -65,6 +65,10 @@ 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 }, @@ -101,6 +105,7 @@ export const paramDef = { repositoryUrl: { type: 'string' }, feedbackUrl: { type: 'string' }, impressumUrl: { type: 'string', nullable: true }, + donationUrl: { type: 'string', nullable: true }, privacyPolicyUrl: { type: 'string', nullable: true }, useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, @@ -119,6 +124,9 @@ 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' }, @@ -279,6 +287,22 @@ 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; } @@ -383,6 +407,10 @@ export default class extends Endpoint { // eslint- set.impressumUrl = ps.impressumUrl; } + if (ps.donationUrl !== undefined) { + set.donationUrl = ps.donationUrl; + } + if (ps.privacyPolicyUrl !== undefined) { set.privacyPolicyUrl = ps.privacyPolicyUrl; } @@ -471,6 +499,26 @@ 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; } diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 0bf2688b4..7293c2e39 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -14,6 +14,7 @@ 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 = { @@ -92,7 +93,7 @@ export default class extends Endpoint { // eslint- antenna.isActive = true; antenna.lastUsedAt = new Date(); - this.antennasRepository.update(antenna.id, antenna); + trackPromise(this.antennasRepository.update(antenna.id, antenna)); if (needPublishEvent) { this.globalEventService.publishInternalEvent('antennaUpdated', antenna); diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts new file mode 100644 index 000000000..0cba129a0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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 = { + tags: [], + + 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' }, + score: { type: 'integer' }, + user: { 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, { detail: false }); + + 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 new file mode 100644 index 000000000..af0f69e4a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/bubble-game/register.ts @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey 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 { BubbleGameRecordsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: [], + + 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', + }, + }, + + res: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + score: { type: 'integer', minimum: 0 }, + seed: { type: 'string', minLength: 1, maxLength: 1024 }, + logs: { type: 'array' }, + 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); + } + + // シードが古すぎる(1時間以上前)のも弾く + if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60) { + 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/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 617ca6573..fcb4fc565 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -41,6 +41,7 @@ export const paramDef = { subscribing: { type: 'boolean', nullable: true }, publishing: { type: 'boolean', nullable: true }, nsfw: { type: 'boolean', nullable: true }, + bubble: { type: 'boolean', nullable: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, offset: { type: 'integer', default: 0 }, sort: { @@ -148,6 +149,23 @@ export default class extends Endpoint { // eslint- } } + if (typeof ps.bubble === 'boolean') { + const meta = await this.metaService.fetch(true); + + if (ps.bubble) { + if (meta.bubbleInstances.length === 0) { + return []; + } + query.andWhere('instance.host IN (:...bubble)', { + bubble: meta.bubbleInstances, + }); + } else if (meta.bubbleInstances.length > 0) { + query.andWhere('instance.host NOT IN (:...bubble)', { + bubble: meta.bubbleInstances, + }); + } + } + if (typeof ps.federating === 'boolean') { if (ps.federating) { query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))'); diff --git a/packages/backend/src/server/api/endpoints/i/export-clips.ts b/packages/backend/src/server/api/endpoints/i/export-clips.ts new file mode 100644 index 000000000..9435a2b23 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/export-clips.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 9ba22f89b..af779aa85 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -7,7 +7,6 @@ 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 { UserEntityService } from '@/core/entities/UserEntityService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -116,6 +115,18 @@ 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, @@ -280,6 +291,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + donationUrl: { + type: 'string', + optional: false, nullable: true, + }, logoImageUrl: { type: 'string', optional: false, nullable: true, @@ -354,12 +369,16 @@ export default class extends Endpoint { // eslint- repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, impressumUrl: instance.impressumUrl, + donationUrl: instance.donationUrl, privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, approvalRequiredForSignup: instance.approvalRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, + enableMcaptcha: instance.enableMcaptcha, + mcaptchaSiteKey: instance.mcaptchaSitekey, + mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl, enableRecaptcha: instance.enableRecaptcha, enableAchievements: instance.enableAchievements, recaptchaSiteKey: instance.recaptchaSiteKey, @@ -375,7 +394,7 @@ export default class extends Endpoint { // eslint- iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, + maxNoteTextLength: this.config.maxNoteLength, // クライアントの手間を減らすためあらかじめJSONに変換しておく defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 27743dfff..3fef0e6aa 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -11,7 +11,7 @@ import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesR import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiChannel } from '@/models/Channel.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import type { Config } from '@/config.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; @@ -82,6 +82,12 @@ export const meta = { id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', }, + maxLength: { + message: 'You tried posting a note which is too long.', + code: 'MAX_LENGTH', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16', + }, + cannotCreateAlreadyExpiredPoll: { message: 'Poll is already expired.', code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', @@ -136,7 +142,6 @@ export const paramDef = { text: { type: 'string', minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true, }, fileIds: { @@ -162,7 +167,7 @@ export const paramDef = { uniqueItems: true, minItems: 2, maxItems: 10, - items: { type: 'string', minLength: 1, maxLength: 50 }, + items: { type: 'string', minLength: 1, maxLength: 150 }, }, multiple: { type: 'boolean' }, expiresAt: { type: 'integer', nullable: true }, @@ -184,6 +189,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -203,6 +211,10 @@ export default class extends Endpoint { // eslint- private noteCreateService: NoteCreateService, ) { super(meta, paramDef, async (ps, me) => { + if (ps.text && (ps.text.length > this.config.maxNoteLength)) { + throw new ApiError(meta.errors.maxLength); + } + let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { visibleUsers = await this.usersRepository.findBy({ diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index cfbc20785..0c9c0d3ba 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -6,7 +6,7 @@ import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesR import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiChannel } from '@/models/Channel.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import type { Config } from '@/config.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEditService } from '@/core/NoteEditService.js'; @@ -135,6 +135,12 @@ export const meta = { code: 'CANNOT_QUOTE_THE_CURRENT_NOTE', id: '33510210-8452-094c-6227-4a6c05d99f02', }, + + maxLength: { + message: 'You tried posting a note which is too long.', + code: 'MAX_LENGTH', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16', + }, }, } as const; @@ -163,7 +169,6 @@ export const paramDef = { text: { type: 'string', minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true, }, fileIds: { @@ -205,7 +210,6 @@ export const paramDef = { text: { type: 'string', minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false, }, }, @@ -236,6 +240,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -255,6 +262,9 @@ export default class extends Endpoint { // eslint- private noteEditService: NoteEditService, ) { super(meta, paramDef, async (ps, me) => { + if (ps.text && (ps.text.length > this.config.maxNoteLength)) { + throw new ApiError(meta.errors.maxLength); + } let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { visibleUsers = await this.usersRepository.findBy({ 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 b43ab044f..be33edec0 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -19,8 +19,8 @@ export const meta = { limit: { duration: ms('1hour'), - max: 60, - minInterval: ms('3sec'), + max: 80, + minInterval: ms('1sec'), }, errors: { diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 5bb819654..b3107f675 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { GetterService } from '@/server/api/GetterService.js'; +import { DI } from '@/di-symbols.js'; +import type { NotesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -40,14 +42,27 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + private noteEntityService: NoteEntityService, - private getterService: GetterService, + private queryService: QueryService, ) { 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; - }); + const query = await this.notesRepository.createQueryBuilder('note') + .where('note.id = :noteId', { noteId: ps.noteId }); + + this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } + + const note = await query.getOne(); + + if (note === null) { + throw new ApiError(meta.errors.noSuchNote); + } return await this.noteEntityService.pack(note, me, { detail: true, diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index 61713b341..efb39ef93 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -1,5 +1,5 @@ import { Entity } from 'megalodon'; -import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from '@/const.js'; +import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import type { Config } from '@/config.js'; import type { MiMeta } from '@/models/Meta.js'; @@ -35,7 +35,7 @@ export async function getInstance( max_featured_tags: 20, }, statuses: { - max_characters: MAX_NOTE_TEXT_LENGTH, + max_characters: config.maxNoteLength, max_media_attachments: 16, characters_reserved_per_url: response.uri.length, }, 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 909b5a5e0..e0245814c 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -21,6 +21,7 @@ class UserListChannel extends Channel { private membershipsMap: Record | undefined> = {}; private listUsersClock: NodeJS.Timeout; private withFiles: boolean; + private withRenotes: boolean; constructor( private userListsRepository: UserListsRepository, @@ -39,6 +40,7 @@ class UserListChannel extends Channel { public async init(params: any) { this.listId = params.listId as string; this.withFiles = params.withFiles ?? false; + this.withRenotes = params.withRenotes ?? true; // Check existence and owner const listExist = await this.userListsRepository.exist({ @@ -104,6 +106,8 @@ class UserListChannel extends Channel { } } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index d857f0bdd..6de903862 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -131,7 +131,7 @@ export class OAuth2ProviderService { fastify.register(multer.contentParser); - fastify.get('/oauth/authorize', async (request, reply) => { + fastify.get('/authorize', async (request, reply) => { const query: any = request.query; let param = "mastodon=true"; if (query.state) param += `&state=${query.state}`; @@ -142,7 +142,7 @@ export class OAuth2ProviderService { ); }); - fastify.get('/oauth/authorize/', async (request, reply) => { + fastify.get('/authorize/', async (request, reply) => { const query: any = request.query; let param = "mastodon=true"; if (query.state) param += `&state=${query.state}`; @@ -153,7 +153,7 @@ export class OAuth2ProviderService { ); }); - fastify.post('/oauth/token', { preHandler: upload.none() }, async (request, reply) => { + fastify.post('/token', { preHandler: upload.none() }, async (request, reply) => { const body: any = request.body || request.query; if (body.grant_type === "client_credentials") { const ret = { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index aa696046e..280bdb20b 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -327,6 +327,21 @@ export class ClientServerService { }); }); + fastify.get<{ Params: { path: string } }>('/tossface/:path(.*)', async (request, reply) => { + const path = request.params.path; + + if (!path.match(/^[0-9a-f-]+\.svg$/)) { + reply.code(404); + return; + } + + reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); + + return await reply.sendFile(path, `${_dirname}/../../../../../tossface-emojis/dist`, { + maxAge: ms('30 days'), + }); + }); + fastify.get<{ Params: { path: string } }>('/twemoji-badge/:path(.*)', async (request, reply) => { const path = request.params.path; diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index d590244e3..3fd88355d 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { summaly } from 'summaly'; +import { summaly } from '@misskey-dev/summaly'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; diff --git a/packages/backend/test-server/.eslintrc.cjs b/packages/backend/test-server/.eslintrc.cjs new file mode 100644 index 000000000..c261741a3 --- /dev/null +++ b/packages/backend/test-server/.eslintrc.cjs @@ -0,0 +1,32 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./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/test-server/.swcrc b/packages/backend/test-server/.swcrc new file mode 100644 index 000000000..e3d693516 --- /dev/null +++ b/packages/backend/test-server/.swcrc @@ -0,0 +1,23 @@ +{ + "$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 new file mode 100644 index 000000000..866a7e1f5 --- /dev/null +++ b/packages/backend/test-server/entry.ts @@ -0,0 +1,80 @@ +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/tsconfig.json b/packages/backend/test-server/tsconfig.json new file mode 100644 index 000000000..10313699c --- /dev/null +++ b/packages/backend/test-server/tsconfig.json @@ -0,0 +1,52 @@ +{ + "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/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index ed967d262..165a1055c 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -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, startServer } from '../utils.js'; +import { api, signup } from '../utils.js'; import type { AuthenticationResponseJSON, AuthenticatorAssertionResponseJSON, @@ -19,12 +19,10 @@ import type { PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, } from '@simplewebauthn/typescript-types'; -import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; describe('2要素認証', () => { - let app: INestApplicationContext; - let alice: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; const config = loadConfig(); const password = 'test'; @@ -185,14 +183,9 @@ 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', { password, diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index c0317f143..e63722b24 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -6,24 +6,20 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { inspect } from 'node:util'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import type { Packed } from '@/misc/json-schema.js'; import { - signup, - post, - userList, - page, - role, - startServer, api, - successfulApiCall, failedApiCall, - uploadFile, + post, + role, + signup, + successfulApiCall, testPaginationConsistency, + uploadFile, + userList, } from '../utils.js'; import type * as misskey from 'misskey-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)); @@ -37,7 +33,7 @@ describe('アンテナ', () => { // - srcのenumにgroupが残っている // - userGroupIdが残っている, isActiveがない type Antenna = misskey.entities.Antenna | Packed<'Antenna'>; - type User = misskey.entities.MeSignup; + type User = misskey.entities.SignupResponse; type Note = misskey.entities.Note; // アンテナを作成できる最小のパラメタ @@ -54,8 +50,6 @@ describe('アンテナ', () => { withReplies: false, }; - let app: INestApplicationContext; - let root: User; let alice: User; let bob: User; @@ -79,10 +73,6 @@ 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' }); @@ -136,10 +126,6 @@ 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]) { diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index 33c8d03fd..89d8b4227 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -6,33 +6,22 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, signup } from '../utils.js'; import type * as misskey from 'misskey-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.MeSignup; + let alice: misskey.entities.SignupResponse; /** フォロワー */ - let follower: misskey.entities.MeSignup; + let follower: misskey.entities.SignupResponse; /** 非フォロワー */ - let other: misskey.entities.MeSignup; + let other: misskey.entities.SignupResponse; /** 非フォロワーでもリプライやメンションをされた人 */ - let target: misskey.entities.MeSignup; + let target: misskey.entities.SignupResponse; /** specified mentionでmentionを飛ばされる人 */ - let target2: misskey.entities.MeSignup; + let target2: misskey.entities.SignupResponse; /** public-post */ let pub: any; diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index cf24228b8..25d5bdb17 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -7,27 +7,30 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { IncomingMessage } from 'http'; -import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { + api, + connectStream, + createAppToken, + failedApiCall, + relativeFetch, + signup, + successfulApiCall, + uploadFile, + waitFire, +} from '../utils.js'; import type * as misskey from 'misskey-js'; describe('API', () => { - let app: INestApplicationContext; - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; 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', { diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index 4445d9036..1dfc87c64 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -6,29 +6,21 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Block', () => { - let app: INestApplicationContext; - // alice blocks bob - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; 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', { userId: bob.id, diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index 49092fba6..b679eea8c 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -18,25 +18,13 @@ import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unf 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'; +import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js'; describe('クリップ', () => { type User = Packed<'User'>; type Note = Packed<'Note'>; type Clip = Packed<'Clip'>; - let app: INestApplicationContext; - let alice: User; let bob: User; let aliceNote: Note; @@ -145,7 +133,6 @@ describe('クリップ', () => { }; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); @@ -160,10 +147,6 @@ describe('クリップ', () => { 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]) { diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index 2ef3434bc..b12b062a6 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -10,30 +10,22 @@ 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 { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Endpoints', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; - let dave: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; + let dave: misskey.entities.SignupResponse; 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', { @@ -710,6 +702,18 @@ 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', { fileId: 'kyoppie', diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts new file mode 100644 index 000000000..f9b59144a --- /dev/null +++ b/packages/backend/test/e2e/exports.ts @@ -0,0 +1,193 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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 'misskey-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 () => { + let res = await api('/clips/create', { + name: 'foo', + description: 'bar', + }, alice); + assert.strictEqual(res.status, 200); + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.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 () => { + let 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]) { + res = await api('/clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res.status, 204); + } + + res = await api('/i/export-clips', {}, alice); + assert.strictEqual(res.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 () => { + let res = await api('/clips/create', { + name: 'kawaii', + description: 'kawaii', + }, alice); + assert.strictEqual(res.status, 200); + const clip1 = res.body; + + res = await api('/clips/create', { + name: 'yuri', + description: 'yuri', + }, alice); + assert.strictEqual(res.status, 200); + const clip2 = res.body; + + const note1 = await post(alice, { + text: 'baz1', + }); + + const note2 = await post(alice, { + text: 'baz2', + }); + + res = await api('/clips/add-note', { + clipId: clip1.id, + noteId: note1.id, + }, alice); + assert.strictEqual(res.status, 204); + + res = await api('/clips/add-note', { + clipId: clip2.id, + noteId: note2.id, + }, alice); + assert.strictEqual(res.status, 204); + + 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 () => { + let 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', + }); + + res = await api('/clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res.status, 204); + + 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, '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 251d66276..0d23b4fe6 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -6,9 +6,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js'; +import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; // Request Accept @@ -23,9 +22,7 @@ const HTML = 'text/html; charset=utf-8'; const JSON_UTF8 = 'application/json; charset=utf-8'; describe('Webリソース', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; let aliceUploadedFile: any; let alicesPost: any; let alicePage: any; @@ -34,7 +31,7 @@ describe('Webリソース', () => { let aliceGalleryPost: any; let aliceChannel: any; - let bob: misskey.entities.MeSignup; + let bob: misskey.entities.SignupResponse; type Request = { path: string, @@ -79,7 +76,6 @@ describe('Webリソース', () => { }; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); aliceUploadedFile = await uploadFile(alice); alicesPost = await post(alice, { @@ -96,10 +92,6 @@ describe('Webリソース', () => { 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に該当するページはありませんでした。" diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 1fbd45c74..1fe0478a1 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -6,26 +6,18 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, startServer, simpleGet } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, signup, simpleGet } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('FF visibility', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; 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', { followingVisibility: 'public', diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index b009ef124..393720356 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -3,35 +3,35 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { INestApplicationContext } from '@nestjs/common'; + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { loadConfig } from '@/config.js'; import { MiUser, UsersRepository } from '@/models/_.js'; -import { jobQueue } from '@/boot/common.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 { jobQueue } from '@/boot/common.js'; +import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Account Move', () => { - let app: INestApplicationContext; let jq: INestApplicationContext; let url: URL; 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 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 Users: UsersRepository; beforeAll(async () => { - app = await startServer(); jq = await jobQueue(); + const config = loadConfig(); url = new URL(config.url); const connection = await initTestDb(false); @@ -46,7 +46,7 @@ describe('Account Move', () => { }, 1000 * 60 * 2); afterAll(async () => { - await Promise.all([app.close(), jq.close()]); + await jq.close(); }); describe('Create Alias', () => { diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index a4b57a1eb..5144df5eb 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -6,29 +6,21 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, react, startServer, waitFire } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, react, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Mute', () => { - let app: INestApplicationContext; - // alice mutes carol - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; 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('/mute/create', { userId: carol.id, diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts index 7eed39c5e..934ef0850 100644 --- a/packages/backend/test/e2e/nodeinfo.ts +++ b/packages/backend/test/e2e/nodeinfo.ts @@ -6,20 +6,9 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { relativeFetch, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { relativeFetch } from '../utils.js'; 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 961df99cc..0f2e08e67 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -8,29 +8,22 @@ 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 { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, initTestDb, post, signup, uploadFile, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Note', () => { - let app: INestApplicationContext; let Notes: any; - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; beforeAll(async () => { - app = await startServer(); const connection = await initTestDb(true); Notes = connection.getRepository(MiNote); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - test('投稿できる', async () => { const post = { text: 'test', diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index 3a5e4ebda..df6ff42df 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -11,13 +11,18 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials, ModuleOptions } from 'simple-oauth2'; +import { + AuthorizationCode, + type AuthorizationTokenConfig, + ClientCredentials, + ModuleOptions, + ResourceOwnerPassword, +} from 'simple-oauth2'; import pkceChallenge from 'pkce-challenge'; import { JSDOM } from 'jsdom'; -import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify'; -import { api, port, signup, startServer } from '../utils.js'; +import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify'; +import { api, port, sendEnvUpdateRequest, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; -import type { INestApplicationContext } from '@nestjs/common'; const host = `http://127.0.0.1:${port}`; @@ -75,7 +80,7 @@ function getMeta(html: string): { transactionId: string | undefined, clientName: }; } -function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { +function fetchDecision(transactionId: string, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise { return fetch(new URL('/oauth/decision', host), { method: 'post', body: new URLSearchParams({ @@ -90,14 +95,14 @@ function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { }); } -async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { +async function fetchDecisionFromResponse(response: Response, user: misskey.entities.SignupResponse, { 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.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { +async function fetchAuthorizationCode(user: misskey.entities.SignupResponse, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { const client = new AuthorizationCode(clientConfig); const response = await fetch(client.authorizeURL({ @@ -147,16 +152,14 @@ async function assertDirectError(response: Response, status: number, error: stri } describe('OAuth', () => { - let app: INestApplicationContext; let fastify: FastifyInstance; - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; let sender: (reply: FastifyReply) => void; beforeAll(async () => { - app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); @@ -168,7 +171,7 @@ describe('OAuth', () => { }, 1000 * 60 * 2); beforeEach(async () => { - process.env.MISSKEY_TEST_CHECK_IP_RANGE = ''; + await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '' }); sender = (reply): void => { reply.send(` @@ -180,7 +183,6 @@ describe('OAuth', () => { afterAll(async () => { await fastify.close(); - await app.close(); }); test('Full flow', async () => { @@ -881,7 +883,7 @@ describe('OAuth', () => { }); test('Disallow loopback', async () => { - process.env.MISSKEY_TEST_CHECK_IP_RANGE = '1'; + await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '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 7d57ba17b..42cc414c3 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -6,29 +6,21 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, signup, sleep, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Renote Mute', () => { - let app: INestApplicationContext; - // alice mutes carol - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; 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', { userId: carol.id, diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 288c54bdb..b6f584fa7 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -8,12 +8,10 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { WebSocket } from 'ws'; import { MiFollowing } from '@/models/Following.js'; -import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Streaming', () => { - let app: INestApplicationContext; let Followings: any; const follow = async (follower: any, followee: any) => { @@ -32,15 +30,15 @@ describe('Streaming', () => { describe('Streaming', () => { // Local users - let ayano: misskey.entities.MeSignup; - let kyoko: misskey.entities.MeSignup; - let chitose: misskey.entities.MeSignup; - let kanako: misskey.entities.MeSignup; + let ayano: misskey.entities.SignupResponse; + let kyoko: misskey.entities.SignupResponse; + let chitose: misskey.entities.SignupResponse; + let kanako: misskey.entities.SignupResponse; // Remote users - let akari: misskey.entities.MeSignup; - let chinatsu: misskey.entities.MeSignup; - let takumi: misskey.entities.MeSignup; + let akari: misskey.entities.SignupResponse; + let chinatsu: misskey.entities.SignupResponse; + let takumi: misskey.entities.SignupResponse; let kyokoNote: any; let kanakoNote: any; @@ -48,7 +46,6 @@ describe('Streaming', () => { let list: any; beforeAll(async () => { - app = await startServer(); const connection = await initTestDb(true); Followings = connection.getRepository(MiFollowing); @@ -95,10 +92,6 @@ describe('Streaming', () => { }, chitose); }, 1000 * 60 * 2); - afterAll(async () => { - await app.close(); - }); - describe('Events', () => { test('mention event', async () => { const fired = await waitFire( diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 0e487976d..26c30d6c4 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -6,28 +6,20 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, connectStream, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, connectStream, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Note thread mute', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; - let bob: misskey.entities.MeSignup; - let carol: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + let carol: misskey.entities.SignupResponse; 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' }); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index cb9558b41..88f89c4a6 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -6,12 +6,8 @@ // 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 { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js'; function genHost() { return randomString() + '.example.com'; @@ -21,16 +17,6 @@ function waitForPushToTl() { return sleep(500); } -let app: INestApplicationContext; - -beforeAll(async () => { - app = await startServer(); -}, 1000 * 60 * 2); - -afterAll(async () => { - await app.close(); -}); - describe('Timelines', () => { describe('Home TL', () => { test.concurrent('自分の visibility: followers なノートが含まれる', async () => { @@ -334,8 +320,9 @@ describe('Timelines', () => { 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 sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); @@ -348,8 +335,9 @@ describe('Timelines', () => { 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 sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); @@ -762,8 +750,9 @@ describe('Timelines', () => { 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 sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); @@ -776,8 +765,9 @@ describe('Timelines', () => { 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 sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index b5f00a632..07da0db36 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -6,20 +6,16 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, uploadUrl, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { api, post, signup, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('users/notes', () => { - let app: INestApplicationContext; - - let alice: misskey.entities.MeSignup; + let alice: misskey.entities.SignupResponse; 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/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); @@ -34,10 +30,6 @@ describe('users/notes', () => { }); }, 1000 * 60 * 2); - afterAll(async() => { - await app.close(); - }); - test('withFiles', async () => { const res = await api('/users/notes', { userId: alice.id, diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index be6f0ec85..572674d81 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -8,20 +8,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { inspect } from 'node:util'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import type { Packed } from '@/misc/json-schema.js'; -import { - signup, - post, - page, - role, - startServer, - api, - successfulApiCall, - failedApiCall, - uploadFile, -} from '../utils.js'; +import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; -import type { INestApplicationContext } from '@nestjs/common'; describe('ユーザー', () => { // エンティティとしてのユーザーを主眼においたテストを記述する @@ -188,8 +176,6 @@ describe('ユーザー', () => { }); }; - let app: INestApplicationContext; - let root: User; let alice: User; let aliceNote: misskey.entities.Note; @@ -233,10 +219,6 @@ describe('ユーザー', () => { let userFollowRequesting: User; let userFollowRequested: User; - beforeAll(async () => { - app = await startServer(); - }, 1000 * 60 * 2); - beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); @@ -324,10 +306,6 @@ describe('ユーザー', () => { await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting); }, 1000 * 60 * 10); - afterAll(async () => { - await app.close(); - }); - beforeEach(async () => { alice = { ...alice, diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts index 14e32e162..0429b7c8b 100644 --- a/packages/backend/test/e2e/well-known.ts +++ b/packages/backend/test/e2e/well-known.ts @@ -6,24 +6,16 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { host, origin, relativeFetch, signup, startServer } from '../utils.js'; -import type { INestApplicationContext } from '@nestjs/common'; +import { host, origin, relativeFetch, signup } from '../utils.js'; import type * as misskey from 'misskey-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/jest.setup.ts b/packages/backend/test/jest.setup.ts new file mode 100644 index 000000000..cf5b9bf24 --- /dev/null +++ b/packages/backend/test/jest.setup.ts @@ -0,0 +1,8 @@ +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 7cba7a2aa..7ee65d1ab 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -15,7 +15,13 @@ 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 { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { + FollowRequestsRepository, + NoteReactionsRepository, + NotesRepository, + PollsRepository, + UsersRepository, +} from '@/models/_.js'; type MockResponse = { type: string; diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts index f2aa5d35e..f02c4e670 100644 --- a/packages/backend/test/unit/AnnouncementService.ts +++ b/packages/backend/test/unit/AnnouncementService.ts @@ -10,7 +10,13 @@ import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; import { AnnouncementService } from '@/core/AnnouncementService.js'; -import type { MiAnnouncement, AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository, MiUser } from '@/models/_.js'; +import type { + AnnouncementReadsRepository, + AnnouncementsRepository, + MiAnnouncement, + MiUser, + UsersRepository, +} from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { genAidx } from '@/misc/id/aidx.js'; import { CacheService } from '@/core/CacheService.js'; diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts index 7234da2e3..64397a1a4 100644 --- a/packages/backend/test/unit/DriveService.ts +++ b/packages/backend/test/unit/DriveService.ts @@ -6,7 +6,13 @@ process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; -import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3'; +import { + DeleteObjectCommand, + DeleteObjectCommandOutput, + InvalidObjectState, + NoSuchKey, + 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 34200899d..cddc374f9 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -55,7 +55,8 @@ describe('FetchInstanceMetadataService', () => { return { fetch: jest.fn() }; } else if (token === DI.redis) { return mockRedis; - }}) + } + }) .compile(); app.enableShutdownHooks(); diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index ba524adff..8604db2ee 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; -import { describe, beforeAll, afterAll, test } from '@jest/globals'; +import { afterAll, beforeAll, describe, test } from '@jest/globals'; import { GlobalModule } from '@/GlobalModule.js'; import { FileInfoService } from '@/core/FileInfoService.js'; //import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/test/unit/MetaService.ts b/packages/backend/test/unit/MetaService.ts index ab30f4828..c4c7f2191 100644 --- a/packages/backend/test/unit/MetaService.ts +++ b/packages/backend/test/unit/MetaService.ts @@ -6,15 +6,13 @@ 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 { DataSource } from 'typeorm'; import type { TestingModule } from '@nestjs/testing'; +import type { DataSource } from 'typeorm'; describe('MetaService', () => { let app: TestingModule; diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 9879eb8e3..46613c29c 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -11,7 +11,7 @@ import { Test } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; -import type { MiRole, RolesRepository, RoleAssignmentsRepository, UsersRepository, MiUser } from '@/models/_.js'; +import type { MiRole, MiUser, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { genAidx } from '@/misc/id/aidx.js'; diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts index c1eafc96b..2ffc99380 100644 --- a/packages/backend/test/unit/S3Service.ts +++ b/packages/backend/test/unit/S3Service.ts @@ -6,7 +6,13 @@ process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; -import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { + CompleteMultipartUploadCommand, + CreateMultipartUploadCommand, + PutObjectCommand, + S3Client, + UploadPartCommand, +} 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/misc/id.ts b/packages/backend/test/unit/misc/id.ts index 59783a9fa..1498c075a 100644 --- a/packages/backend/test/unit/misc/id.ts +++ b/packages/backend/test/unit/misc/id.ts @@ -4,13 +4,13 @@ */ import { ulid } from 'ulid'; -import { describe, test, expect } from '@jest/globals'; +import { describe, expect, test } 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 { ulidRegExp, parseUlid } from '@/misc/id/ulid.js'; +import { parseUlid, ulidRegExp } from '@/misc/id/ulid.js'; describe('misc:id', () => { test('aid', () => { diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts index b16d26d86..caa815b3d 100644 --- a/packages/backend/test/unit/misc/others.ts +++ b/packages/backend/test/unit/misc/others.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { describe, test, expect } from '@jest/globals'; +import { describe, expect, test } 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 46b8ea9cd..2b232a0a5 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -5,7 +5,7 @@ import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; -import { isAbsolute, basename } from 'node:path'; +import { basename, isAbsolute } from 'node:path'; import { randomUUID } from 'node:crypto'; import { inspect } from 'node:util'; import WebSocket, { ClientOptions } from 'ws'; @@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; -export { server as startServer } from '@/boot/common.js'; +export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; interface UserToken { token: string; @@ -68,7 +68,11 @@ export const failedApiCall = async (request: ApiRequest, assertion: { return res.body; }; -const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => { +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', @@ -275,7 +279,11 @@ interface UploadOptions { * Upload file * @param user User */ -export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | 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/Lenna.jpg', import.meta.url) : isAbsolute(path.toString()) @@ -426,8 +434,8 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde ]; const body = - jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : - htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : + jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : + htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : null; return { @@ -557,3 +565,34 @@ export function sleep(msec: number) { }, msec); }); } + +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.'); + } +} diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index ad2cf18a3..8f32c6e62 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -28,6 +28,8 @@ const keys = [ 'd-cherry', 'd-ice', 'd-u0', + 'rosepine', + 'rosepine-dawn', ] await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { diff --git a/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 new file mode 100644 index 000000000..cafc34ad9 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/bubble2.mp3 b/packages/frontend/assets/drop-and-fusion/bubble2.mp3 new file mode 100644 index 000000000..8b4f8df6e Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/bubble2.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/click.mp3 b/packages/frontend/assets/drop-and-fusion/click.mp3 new file mode 100644 index 000000000..ef03e60f6 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/click.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/cold_face.png b/packages/frontend/assets/drop-and-fusion/cold_face.png new file mode 100644 index 000000000..f5f53e9ef Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/cold_face.png differ diff --git a/packages/frontend/assets/drop-and-fusion/drop-arrow.svg b/packages/frontend/assets/drop-and-fusion/drop-arrow.svg new file mode 100644 index 000000000..f98bb8a1a --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/drop-arrow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/frontend/assets/drop-and-fusion/dropper.png b/packages/frontend/assets/drop-and-fusion/dropper.png new file mode 100644 index 000000000..f4300aa5c Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/dropper.png differ diff --git a/packages/frontend/assets/drop-and-fusion/exploding_head.png b/packages/frontend/assets/drop-and-fusion/exploding_head.png new file mode 100644 index 000000000..e8ec5182c Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/exploding_head.png differ diff --git a/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png b/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png new file mode 100644 index 000000000..c523020f6 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png differ diff --git a/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png b/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png new file mode 100644 index 000000000..db9e839c8 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png differ diff --git a/packages/frontend/assets/drop-and-fusion/frame-dark.svg b/packages/frontend/assets/drop-and-fusion/frame-dark.svg new file mode 100644 index 000000000..3fa7c0da8 --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/frame-dark.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend/assets/drop-and-fusion/frame-light.svg b/packages/frontend/assets/drop-and-fusion/frame-light.svg new file mode 100644 index 000000000..6052ccbaa --- /dev/null +++ b/packages/frontend/assets/drop-and-fusion/frame-light.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend/assets/drop-and-fusion/gameover.mp3 b/packages/frontend/assets/drop-and-fusion/gameover.mp3 new file mode 100644 index 000000000..23b41c569 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/gameover.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/gameover.png b/packages/frontend/assets/drop-and-fusion/gameover.png new file mode 100644 index 000000000..8b622577c Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/gameover.png differ diff --git a/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png b/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png new file mode 100644 index 000000000..fd72d749a Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png differ diff --git a/packages/frontend/assets/drop-and-fusion/heart_suit.png b/packages/frontend/assets/drop-and-fusion/heart_suit.png new file mode 100644 index 000000000..b0105f858 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/heart_suit.png differ diff --git a/packages/frontend/assets/drop-and-fusion/hold.mp3 b/packages/frontend/assets/drop-and-fusion/hold.mp3 new file mode 100644 index 000000000..f064c976d Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/hold.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_1.png b/packages/frontend/assets/drop-and-fusion/keycap_1.png new file mode 100644 index 000000000..d672f2854 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_1.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_10.png b/packages/frontend/assets/drop-and-fusion/keycap_10.png new file mode 100644 index 000000000..32cf19354 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_10.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_2.png b/packages/frontend/assets/drop-and-fusion/keycap_2.png new file mode 100644 index 000000000..81c3f58e6 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_2.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_3.png b/packages/frontend/assets/drop-and-fusion/keycap_3.png new file mode 100644 index 000000000..424d8c123 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_3.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_4.png b/packages/frontend/assets/drop-and-fusion/keycap_4.png new file mode 100644 index 000000000..ea6ae5053 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_4.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_5.png b/packages/frontend/assets/drop-and-fusion/keycap_5.png new file mode 100644 index 000000000..ad435da69 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_5.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_6.png b/packages/frontend/assets/drop-and-fusion/keycap_6.png new file mode 100644 index 000000000..70c9522b4 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_6.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_7.png b/packages/frontend/assets/drop-and-fusion/keycap_7.png new file mode 100644 index 000000000..5a2430748 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_7.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_8.png b/packages/frontend/assets/drop-and-fusion/keycap_8.png new file mode 100644 index 000000000..9689d8ecf Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_8.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_9.png b/packages/frontend/assets/drop-and-fusion/keycap_9.png new file mode 100644 index 000000000..ac3f63884 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/keycap_9.png differ diff --git a/packages/frontend/assets/drop-and-fusion/logo.png b/packages/frontend/assets/drop-and-fusion/logo.png new file mode 100644 index 000000000..c6725bea8 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/logo.png differ diff --git a/packages/frontend/assets/drop-and-fusion/pleading_face.png b/packages/frontend/assets/drop-and-fusion/pleading_face.png new file mode 100644 index 000000000..42f58d411 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/pleading_face.png differ diff --git a/packages/frontend/assets/drop-and-fusion/poi1.mp3 b/packages/frontend/assets/drop-and-fusion/poi1.mp3 new file mode 100644 index 000000000..59dae9096 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/poi1.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/poi2.mp3 b/packages/frontend/assets/drop-and-fusion/poi2.mp3 new file mode 100644 index 000000000..a65c65389 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/poi2.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png b/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png new file mode 100644 index 000000000..416ef0410 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png differ diff --git a/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png b/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png new file mode 100644 index 000000000..c0f72254c Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png differ diff --git a/packages/frontend/assets/drop-and-fusion/zany_face.png b/packages/frontend/assets/drop-and-fusion/zany_face.png new file mode 100644 index 000000000..f14f9db20 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/zany_face.png differ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 68aa501c8..5b4c2544d 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "watch": "vite", - "dev": "vite --config vite.config.local-dev.ts", + "dev": "vite --config vite.config.local-dev.ts --debug hmr", "build": "vite build", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", @@ -19,19 +19,20 @@ "dependencies": { "@discordapp/twemoji": "15.0.2", "@github/webauthn-json": "2.1.1", + "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", + "@misskey-dev/browser-image-resizer": "2.2.1-misskey.10", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.5", "@rollup/pluginutils": "5.1.0", - "@sharkey/sfm-js": "0.24.3", - "@syuilo/aiscript": "0.16.0", + "@sharkey/sfm-js": "0.24.4", + "@syuilo/aiscript": "0.17.0", "@phosphor-icons/web": "^2.0.3", "@twemoji/parser": "15.0.0", - "@vitejs/plugin-vue": "4.5.2", - "@vue/compiler-sfc": "3.3.12", + "@vitejs/plugin-vue": "5.0.2", + "@vue/compiler-sfc": "3.4.3", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", "astring": "1.8.6", "broadcast-channel": "7.0.0", - "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "buraha": "0.0.1", "canvas-confetti": "1.6.1", "chart.js": "4.4.1", @@ -46,7 +47,6 @@ "escape-regexp": "0.0.1", "estree-walker": "3.0.3", "eventemitter3": "5.0.1", - "gsap": "3.12.4", "idb-keyval": "6.2.1", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", @@ -59,6 +59,7 @@ "rollup": "4.9.1", "sanitize-html": "2.11.0", "sass": "1.69.5", + "seedrandom": "^3.0.5", "shiki": "0.14.7", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", @@ -71,10 +72,12 @@ "uuid": "9.0.1", "v-code-diff": "1.7.2", "vite": "5.0.10", - "vue": "3.3.12", + "vue": "3.4.3", "vuedraggable": "next" }, "devDependencies": { + "@misskey-dev/eslint-plugin": "^1.0.0", + "@misskey-dev/summaly": "^5.0.3", "@storybook/addon-actions": "7.6.5", "@storybook/addon-essentials": "7.6.5", "@storybook/addon-interactions": "7.6.5", @@ -108,7 +111,7 @@ "@typescript-eslint/eslint-plugin": "6.14.0", "@typescript-eslint/parser": "6.14.0", "@vitest/coverage-v8": "0.34.6", - "@vue/runtime-core": "3.3.12", + "@vue/runtime-core": "3.4.3", "acorn": "8.11.2", "cross-env": "7.0.3", "cypress": "13.6.1", @@ -128,11 +131,10 @@ "start-server-and-test": "2.0.3", "storybook": "7.6.5", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", - "summaly": "github:misskey-dev/summaly", "vite-plugin-turbosnap": "1.0.3", "vitest": "0.34.6", "vitest-fetch-mock": "0.2.2", "vue-eslint-parser": "9.3.2", - "vue-tsc": "1.8.25" + "vue-tsc": "1.8.27" } } diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 05008194f..de0a2da48 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -11,7 +11,8 @@ import { miLocalStorage } from '@/local-storage.js'; import { MenuButton } from '@/types/menu.js'; import { del, get, set } from '@/scripts/idb-proxy.js'; import { apiUrl } from '@/config.js'; -import { waiting, api, popup, popupMenu, success, alert } from '@/os.js'; +import { waiting, popup, popupMenu, success, alert } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; // TODO: 他のタブと永続化されたstateを同期 @@ -23,9 +24,14 @@ const accountData = miLocalStorage.getItem('account'); // TODO: 外部からはreadonlyに export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; -export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); +export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true); export const iAmAdmin = $i != null && $i.isAdmin; +export function signinRequired() { + if ($i == null) throw new Error('signin required'); + return $i; +} + export let notesCount = $i == null ? 0 : $i.notesCount; export function incNotesCount() { notesCount++; @@ -246,7 +252,7 @@ export async function openAccountMenu(opts: { } const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id)); - const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) }); + const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) }); function createItem(account: Misskey.entities.UserDetailed) { return { diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 63f169d9a..2d1d0b801 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; +import { setupRouter } from '@/global/router/definition.js'; export async function common(createVue: () => App) { console.info(`Sharkey v${version}`); @@ -245,6 +246,8 @@ export async function common(createVue: () => App) { const app = createVue(); + setupRouter(app); + if (_DEV_) { app.config.performance = true; } diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index cdc1d11ca..f248bc10e 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -3,23 +3,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { createApp, markRaw, defineAsyncComponent } from 'vue'; +import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { common } from './common.js'; import { ui } from '@/config.js'; import { i18n } from '@/i18n.js'; -import { confirm, alert, post, popup, toast } from '@/os.js'; +import { alert, confirm, popup, post, toast } from '@/os.js'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; -import { $i, updateAccount, signout } from '@/account.js'; -import { defaultStore, ColdDeviceStorage } from '@/store.js'; +import { $i, signout, updateAccount } from '@/account.js'; +import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { makeHotkey } from '@/scripts/hotkey.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; -import { mainRouter } from '@/router.js'; import { initializeSw } from '@/scripts/initialize-sw.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; +import { mainRouter } from '@/global/router/main.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -269,7 +269,7 @@ export async function mainBoot() { main.on('unreadAntenna', () => { updateAccount({ hasUnreadAntenna: true }); - sound.play('antenna'); + sound.playMisskeySfx('antenna'); }); main.on('readAllAnnouncements', () => { diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts index 25d2b3c15..20950add8 100644 --- a/packages/frontend/src/cache.ts +++ b/packages/frontend/src/cache.ts @@ -5,9 +5,9 @@ import * as Misskey from 'misskey-js'; import { Cache } from '@/scripts/cache.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; -export const clipsCache = new Cache(1000 * 60 * 30, () => api('clips/list')); -export const rolesCache = new Cache(1000 * 60 * 30, () => api('admin/roles/list')); -export const userListsCache = new Cache(1000 * 60 * 30, () => api('users/lists/list')); -export const antennasCache = new Cache(1000 * 60 * 30, () => api('antennas/list')); +export const clipsCache = new Cache(1000 * 60 * 30, () => misskeyApi('clips/list')); +export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list')); +export const userListsCache = new Cache(1000 * 60 * 30, () => misskeyApi('users/lists/list')); +export const antennasCache = new Cache(1000 * 60 * 30, () => misskeyApi('antennas/list')); diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index b11cf1c8a..d0d67661f 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js'; import MkMention from './MkMention.vue'; import { i18n } from '@/i18n.js'; import { host as localHost } from '@/config.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const user = ref(); @@ -25,7 +25,7 @@ const props = defineProps<{ movedTo: string; // user id }>(); -api('users/show', { userId: props.movedTo }).then(u => user.value = u); +misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u); diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index eeb9325a2..27e5bb1b8 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -13,8 +13,10 @@ SPDX-License-Identifier: AGPL-3.0-only >
- - {{ text }} +
@@ -53,6 +55,7 @@ const el = shallowRef(); const zIndex = os.claimZIndex('high'); function setPosition() { + if (!el.value || !props.targetElement) return; const data = calcPopupPosition(el.value, { anchorElement: props.targetElement, direction: props.direction, diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 07efaf898..b5f657e95 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -4,12 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only --> @@ -26,7 +26,7 @@ import { confetti } from '@/scripts/confetti.js'; const modal = shallowRef>(); const whatIsNew = () => { - modal.value.close(); + modal.value?.close(); window.open(`https://git.joinsharkey.org/Sharkey/Sharkey/releases/tag/${version}`, '_blank'); }; diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 486aaa0bb..81d0acb8f 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue index d69e5fece..9cfc33269 100644 --- a/packages/frontend/src/components/SkInstanceTicker.vue +++ b/packages/frontend/src/components/SkInstanceTicker.vue @@ -46,11 +46,22 @@ const bg = { align-items: center; height: 1.5ex; border-radius: var(--radius-xl); - margin-top: 5px; padding: 4px; overflow: clip; color: #fff; - text-shadow: -1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000; + text-shadow: /* .866 ≈ sin(60deg) */ + 1px 0 1px #000, + .866px .5px 1px #000, + .5px .866px 1px #000, + 0 1px 1px #000, + -.5px .866px 1px #000, + -.866px .5px 1px #000, + -1px 0 1px #000, + -.866px -.5px 1px #000, + -.5px -.866px 1px #000, + 0 -1px 1px #000, + .5px -.866px 1px #000, + .866px -.5px 1px #000; } .icon { @@ -59,7 +70,9 @@ const bg = { } .name { - margin-left: 4px; + padding: 0.5ex; + margin: -0.5ex; + margin-left: calc(4px - 0.5ex); line-height: 1; font-size: 0.8em; font-weight: bold; diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index 83909654c..15192405f 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -190,6 +190,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import * as sound from '@/scripts/sound.js'; import { defaultStore, noteViewInterruptors } from '@/store.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -208,7 +209,7 @@ import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -282,8 +283,8 @@ const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null; const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); -const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text).filter(u => u !== renoteUrl && u !== renoteUri) : null); -const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null); +const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); +const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null); const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong); const isDeleted = ref(false); @@ -320,7 +321,7 @@ const keymap = { }; provide('react', (reaction: string) => { - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -341,7 +342,7 @@ if (props.mock) { if (!props.mock) { useTooltip(renoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, }); @@ -359,7 +360,7 @@ if (!props.mock) { }); useTooltip(quoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { + const renotes = await misskeyApi('notes/renotes', { noteId: appearNote.value.id, limit: 11, quote: true, @@ -378,7 +379,7 @@ if (!props.mock) { }); if ($i) { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -400,39 +401,43 @@ function smallerVisibility(a: Visibility | string, b: Visibility | string): Visi } function boostVisibility() { - os.popupMenu([ - { - type: 'button', - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', - text: i18n.ts._visibility['public'], - action: () => { - renote('public'); + if (!defaultStore.state.showVisibilitySelectorOnBoost) { + renote(defaultStore.state.visibilityOnBoost); + } else { + os.popupMenu([ + { + type: 'button', + icon: 'ph-globe-hemisphere-west ph-bold ph-lg', + text: i18n.ts._visibility['public'], + action: () => { + renote('public'); + }, }, - }, - { - type: 'button', - icon: 'ph-house ph-bold ph-lg', - text: i18n.ts._visibility['home'], - action: () => { - renote('home'); + { + type: 'button', + icon: 'ph-house ph-bold ph-lg', + text: i18n.ts._visibility['home'], + action: () => { + renote('home'); + }, }, - }, - { - type: 'button', - icon: 'ph-lock ph-bold ph-lg', - text: i18n.ts._visibility['followers'], - action: () => { - renote('followers'); + { + type: 'button', + icon: 'ph-lock ph-bold ph-lg', + text: i18n.ts._visibility['followers'], + action: () => { + renote('followers'); + }, }, - }, - { - type: 'button', - icon: 'ph-planet ph-bold ph-lg', - text: i18n.ts._timelines.local, - action: () => { - renote('local'); - }, - }], renoteButton.value); + { + type: 'button', + icon: 'ph-planet ph-bold ph-lg', + text: i18n.ts._timelines.local, + action: () => { + renote('local'); + }, + }], renoteButton.value); + } } function renote(visibility: Visibility | 'local') { @@ -449,7 +454,7 @@ function renote(visibility: Visibility | 'local') { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: appearNote.value.id, channelId: appearNote.value.channelId, }).then(() => { @@ -475,7 +480,7 @@ function renote(visibility: Visibility | 'local') { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { localOnly: visibility === 'local' ? true : localOnlySetting, visibility: noteVisibility, renoteId: appearNote.value.id, @@ -499,7 +504,7 @@ function quote() { renote: appearNote.value, channel: appearNote.value.channel, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -521,7 +526,7 @@ function quote() { os.post({ renote: appearNote.value, }).then(() => { - os.api('notes/renotes', { + misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i.id, limit: 1, @@ -559,10 +564,11 @@ function reply(viaKeyboard = false): void { function like(): void { pleaseLogin(); showMovedDialog(); + sound.playMisskeySfx('reaction'); if (props.mock) { return; } - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -579,13 +585,13 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { - sound.play('reaction'); + sound.playMisskeySfx('reaction'); if (props.mock) { return; } - os.api('notes/like', { + misskeyApi('notes/like', { noteId: appearNote.value.id, override: defaultLike.value, }); @@ -599,14 +605,14 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { - sound.play('reaction'); + sound.playMisskeySfx('reaction'); if (props.mock) { emit('reaction', reaction); return; } - os.api('notes/reactions/create', { + misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, }); @@ -628,7 +634,7 @@ function undoReact(note): void { return; } - os.api('notes/reactions/delete', { + misskeyApi('notes/reactions/delete', { noteId: note.id, }); } @@ -637,7 +643,7 @@ function undoRenote(note) : void { if (props.mock) { return; } - os.api('notes/unrenote', { + misskeyApi('notes/unrenote', { noteId: note.id, }); os.toast(i18n.ts.rmboost); @@ -714,7 +720,7 @@ function showRenoteMenu(viaKeyboard = false): void { icon: 'ph-trash ph-bold ph-lg', danger: true, action: () => { - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: note.value.id, }); isDeleted.value = true; @@ -771,8 +777,12 @@ function focusAfter() { focusNext(el.value); } +function scrollIntoView() { + el.value.scrollIntoView(); +} + function readPromo() { - os.api('promo/read', { + misskeyApi('promo/read', { noteId: appearNote.value.id, }); isDeleted.value = true; @@ -785,6 +795,12 @@ function emitUpdReaction(emoji: string, delta: number) { emit('reaction', emoji); } } + +defineExpose({ + focus, + blur, + scrollIntoView, +}); diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue new file mode 100644 index 000000000..0938ca6a8 --- /dev/null +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -0,0 +1,182 @@ + + + + + + + diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 82cfa92f6..3cb7c6f40 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -39,7 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -82,6 +85,7 @@ import MkInput from '@/components/MkInput.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { customEmojiCategories } from '@/custom-emojis.js'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -104,7 +108,7 @@ const rolesThatCanBeUsedThisEmojiAsReaction = ref([]); const file = ref(); watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { - rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); + rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); }, { immediate: true }); const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); @@ -123,7 +127,7 @@ async function changeImage(ev) { } async function addRole() { - const roles = await os.api('admin/roles/list'); + const roles = await misskeyApi('admin/roles/list'); const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id); const { canceled, result: role } = await os.select({ @@ -185,7 +189,7 @@ async function del() { }); if (canceled) return; - os.api('admin/emoji/delete', { + misskeyApi('admin/emoji/delete', { id: props.emoji.id, }).then(() => { emit('done', { diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index d94fe96fa..6d9778494 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index fbca2b8ed..7038b313f 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -68,7 +68,7 @@ import * as Misskey from 'misskey-js'; import MkUserList from '@/components/MkUserList.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -123,14 +123,14 @@ const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, sort: '+createdAt', } }; -os.api('hashtags/list', { +misskeyApi('hashtags/list', { sort: '+attachedLocalUsers', attachedToLocalUserOnly: true, limit: 30, }).then(tags => { tagsLocal.value = tags; }); -os.api('hashtags/list', { +misskeyApi('hashtags/list', { sort: '+attachedRemoteUsers', attachedToRemoteUserOnly: true, limit: 30, diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 21e6d0061..73ed4945d 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -38,13 +38,14 @@ import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkTextarea from '@/components/MkTextarea.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const PRESET_DEFAULT = `/// @ 0.16.0 @@ -369,7 +370,7 @@ const flash = ref(null); const visibility = ref('public'); if (props.id) { - flash.value = await os.api('flash/show', { + flash.value = await misskeyApi('flash/show', { flashId: props.id, }); } diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 2b9346fca..9f8956eb8 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -42,9 +42,9 @@ import { computed, ref } from 'vue'; import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 5fae1248e..63478edd1 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -62,12 +62,13 @@ import * as Misskey from 'misskey-js'; import { Interpreter, Parser, values } from '@syuilo/aiscript'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { url } from '@/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkAsUi from '@/components/MkAsUi.vue'; import { AsUiComponent, AsUiRoot, registerAsUiLib } from '@/scripts/aiscript/ui.js'; -import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import MkFolder from '@/components/MkFolder.vue'; import MkCode from '@/components/MkCode.vue'; import { defaultStore } from '@/store.js'; @@ -84,7 +85,7 @@ const error = ref(null); function fetchFlash() { flash.value = null; - os.api('flash/show', { + misskeyApi('flash/show', { flashId: props.id, }).then(_flash => { flash.value = _flash; @@ -162,15 +163,7 @@ async function run() { THIS_ID: values.STR(flash.value.id), THIS_URL: values.STR(`${url}/play/${flash.value.id}`), }, { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ result: a }) => { - ok(a ?? ''); - }); - }); - }, + in: aiScriptReadline, out: (value) => { // nop }, diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index d75066422..f7b756ff3 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -41,7 +41,7 @@ import { shallowRef, computed } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { userPage, acct } from '@/filters/user.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { infoImageUrl } from '@/instance.js'; @@ -54,13 +54,13 @@ const pagination = { }; function accept(user) { - os.api('following/requests/accept', { userId: user.id }).then(() => { + misskeyApi('following/requests/accept', { userId: user.id }).then(() => { paginationComponent.value.reload(); }); } function reject(user) { - os.api('following/requests/reject', { userId: user.id }).then(() => { + misskeyApi('following/requests/reject', { userId: user.id }).then(() => { paginationComponent.value.reload(); }); } diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue index a0a4a480b..eefef828b 100644 --- a/packages/frontend/src/pages/follow.vue +++ b/packages/frontend/src/pages/follow.vue @@ -12,9 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only import { } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -import { mainRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from "@/store.js"; +import { defaultStore } from '@/store.js'; +import { mainRouter } from '@/global/router/main.js'; async function follow(user): Promise { const { canceled } = await os.confirm({ @@ -42,7 +43,7 @@ if (acct == null) { let promise; if (acct.startsWith('https://')) { - promise = os.api('ap/show', { + promise = misskeyApi('ap/show', { uri: acct, }); promise.then(res => { @@ -60,7 +61,7 @@ if (acct.startsWith('https://')) { } }); } else { - promise = os.api('users/show', Misskey.acct.parse(acct)); + promise = misskeyApi('users/show', Misskey.acct.parse(acct)); promise.then(user => { follow(user); }); diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index 857317c48..7dfa2b592 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -47,9 +47,10 @@ import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; import { selectFiles } from '@/scripts/select-file.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -107,7 +108,7 @@ async function del() { } watch(() => props.postId, () => { - init.value = () => props.postId ? os.api('gallery/posts/show', { + init.value = () => props.postId ? misskeyApi('gallery/posts/show', { postId: props.postId, }).then(post => { files.value = post.files ?? []; diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 936d9b839..57a282e61 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -53,7 +53,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 54a8790ef..e84959dd5 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -66,18 +66,19 @@ import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import { url } from '@/config.js'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -97,7 +98,7 @@ const otherPostsPagination = { function fetchPost() { post.value = null; - os.api('gallery/posts/show', { + misskeyApi('gallery/posts/show', { postId: props.postId, }).then(_post => { post.value = _post; diff --git a/packages/frontend/src/pages/install-extentions.vue b/packages/frontend/src/pages/install-extentions.vue index 7e6c75ac9..c42a17e84 100644 --- a/packages/frontend/src/pages/install-extentions.vue +++ b/packages/frontend/src/pages/install-extentions.vue @@ -105,6 +105,7 @@ import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js'; import { parseThemeCode, installTheme } from '@/scripts/install-theme.js'; import { unisonReload } from '@/scripts/unison-reload.js'; @@ -159,7 +160,7 @@ async function fetch() { uiPhase.value = 'error'; return; } - const res = await os.api('fetch-external-resources', { + const res = await misskeyApi('fetch-external-resources', { url: url.value, hash: hash.value, }).catch((err) => { diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 683a31c36..ccda42f70 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -130,6 +130,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import number from '@/filters/number.js'; import { iAmModerator, iAmAdmin } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -166,9 +167,9 @@ const usersPagination = { async function fetch(): Promise { if (iAmAdmin) { - meta.value = await os.api('admin/meta'); + meta.value = await misskeyApi('admin/meta'); } - instance.value = await os.api('federation/show-instance', { + instance.value = await misskeyApi('federation/show-instance', { host: props.host, }); suspended.value = instance.value?.isSuspended ?? false; @@ -182,7 +183,7 @@ async function toggleBlock(): Promise { if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; - await os.api('admin/update-meta', { + await misskeyApi('admin/update-meta', { blockedHosts: isBlocked.value ? meta.value.blockedHosts.concat([host]) : meta.value.blockedHosts.filter(x => x !== host), }); } @@ -192,14 +193,14 @@ async function toggleSilenced(): Promise { if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; const silencedHosts = meta.value.silencedHosts ?? []; - await os.api('admin/update-meta', { + await misskeyApi('admin/update-meta', { silencedHosts: isSilenced.value ? silencedHosts.concat([host]) : silencedHosts.filter(x => x !== host), }); } async function toggleSuspend(): Promise { if (!instance.value) throw new Error('No instance?'); - await os.api('admin/federation/update-instance', { + await misskeyApi('admin/federation/update-instance', { host: instance.value.host, isSuspended: suspended.value, }); @@ -207,7 +208,7 @@ async function toggleSuspend(): Promise { async function toggleNSFW(): Promise { if (!instance.value) throw new Error('No instance?'); - await os.api('admin/federation/update-instance', { + await misskeyApi('admin/federation/update-instance', { host: instance.value.host, isNSFW: isNSFW.value, }); @@ -215,7 +216,7 @@ async function toggleNSFW(): Promise { function refreshMetadata(): void { if (!instance.value) throw new Error('No instance?'); - os.api('admin/federation/refresh-remote-instance-metadata', { + misskeyApi('admin/federation/refresh-remote-instance-metadata', { host: instance.value.host, }); os.alert({ diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index 6ac78a206..d20ec1411 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -40,6 +40,7 @@ import { computed, ref, shallowRef } from 'vue'; import type * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkButton from '@/components/MkButton.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkInviteCode from '@/components/MkInviteCode.vue'; @@ -68,7 +69,7 @@ const resetCycle = computed(() => { }); async function create() { - const ticket = await os.api('invite/create'); + const ticket = await misskeyApi('invite/create'); os.alert({ type: 'success', title: i18n.ts.inviteCodeCreated, @@ -87,7 +88,7 @@ function deleted(id: string) { } async function update() { - currentInviteLimit.value = (await os.api('invite/limit')).remaining; + currentInviteLimit.value = (await misskeyApi('invite/limit')).remaining; } update(); diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index d6c6685a7..7cf5025ea 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; @@ -53,12 +54,12 @@ const error = ref(); const users = ref([]); function fetchList(): void { - os.api('users/lists/show', { + misskeyApi('users/lists/show', { listId: props.listId, forPublic: true, }).then(_list => { list.value = _list; - os.api('users/show', { + misskeyApi('users/show', { userIds: list.value.userIds, }).then(_users => { users.value = _users; diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 2b53b67ab..333b485c7 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import MkSignin from '@/components/MkSignin.vue'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i, login } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -65,7 +65,7 @@ const state = ref(null); async function accept(): Promise { state.value = 'waiting'; - await os.api('miauth/gen-token', { + await misskeyApi('miauth/gen-token', { session: props.session, name: props.name, iconUrl: props.icon, diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index 79b592dad..c1f8064c2 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -14,8 +14,8 @@ import { ref } from 'vue'; import XAntenna from './editor.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; import { antennasCache } from '@/cache.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index 851b32527..38bfa9f0c 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -13,11 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import XAntenna from './editor.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { useRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache } from '@/cache.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -32,7 +32,7 @@ function onAntennaUpdated() { router.push('/my/antennas'); } -os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { +misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { antenna.value = antennaResponse; }); diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue index 0fc7f862a..a002286ca 100644 --- a/packages/frontend/src/pages/my-antennas/editor.vue +++ b/packages/frontend/src/pages/my-antennas/editor.vue @@ -57,6 +57,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -84,7 +85,7 @@ const userLists = ref(null); watch(() => src.value, async () => { if (src.value === 'list' && userLists.value === null) { - userLists.value = await os.api('users/lists/list'); + userLists.value = await misskeyApi('users/lists/list'); } }); @@ -119,7 +120,7 @@ async function deleteAntenna() { }); if (canceled) return; - await os.api('antennas/delete', { + await misskeyApi('antennas/delete', { antennaId: props.antenna.id, }); diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index d787e53bb..c4d1f9655 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -32,6 +32,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkClipPreview from '@/components/MkClipPreview.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { clipsCache } from '@/cache.js'; @@ -48,7 +49,7 @@ const favorites = ref(null); const pagingComponent = shallowRef>(); watch(tab, async () => { - favorites.value = await os.api('clips/my-favorites'); + favorites.value = await misskeyApi('clips/my-favorites'); }); async function create() { diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 3379cf43d..25a54375f 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
{{ list.name }} ({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }})
+
{{ list.name }} ({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})
@@ -37,7 +37,9 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { userListsCache } from '@/cache.js'; import { infoImageUrl } from '@/instance.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); const items = computed(() => userListsCache.value.value ?? []); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index df9cdb0fc..4d1e0b487 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
{{ i18n.ts.addUser }} @@ -57,7 +57,7 @@ import { computed, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; -import { mainRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { userPage } from '@/filters/user.js'; @@ -66,9 +66,12 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInput from '@/components/MkInput.vue'; import { userListsCache } from '@/cache.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { defaultStore } from '@/store.js'; import MkPagination from '@/components/MkPagination.vue'; +import { mainRouter } from '@/global/router/main.js'; + +const $i = signinRequired(); const { enableInfiniteScroll, @@ -91,7 +94,7 @@ const membershipsPagination = { }; function fetchList() { - os.api('users/lists/show', { + misskeyApi('users/lists/show', { listId: props.listId, }).then(_list => { list.value = _list; @@ -119,7 +122,7 @@ async function removeUser(item, ev) { danger: true, action: async () => { if (!list.value) return; - os.api('users/lists/pull', { + misskeyApi('users/lists/pull', { listId: list.value.id, userId: item.userId, }).then(() => { @@ -134,7 +137,7 @@ async function showMembershipMenu(item, ev) { text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline, icon: item.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg', action: async () => { - os.api('users/lists/update-membership', { + misskeyApi('users/lists/update-membership', { listId: list.value.id, userId: item.userId, withReplies: !item.withReplies, diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index a98a7bde2..6cc91f417 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -55,7 +55,7 @@ import MkNotes from '@/components/MkNotes.vue'; import SkNoteDetailed from '@/components/SkNoteDetailed.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { dateString } from '@/filters/date.js'; @@ -96,13 +96,13 @@ function fetchNote() { showPrev.value = false; showNext.value = false; note.value = null; - os.api('notes/show', { + misskeyApi('notes/show', { noteId: props.noteId, }).then(res => { note.value = res; // 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) { - os.api('notes/clips', { + misskeyApi('notes/clips', { noteId: note.value.id, }).then((_clips) => { clips.value = _clips; diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue index 459454a9b..4637c20fc 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue @@ -26,6 +26,7 @@ import * as Misskey from 'misskey-js'; import XContainer from '../page-editor.container.vue'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -52,7 +53,7 @@ onMounted(async () => { if (props.modelValue.fileId == null) { await choose(); } else { - os.api('drive/files/show', { + misskeyApi('drive/files/show', { fileId: props.modelValue.fileId, }).then(fileResponse => { file.value = fileResponse; diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue index 442558cc2..b82e57fe4 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue @@ -30,7 +30,7 @@ import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkNote from '@/components/MkNote.vue'; import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -53,7 +53,7 @@ watch(id, async () => { ...props.modelValue, note: id.value, }); - note.value = await os.api('notes/show', { noteId: id.value }); + note.value = await misskeyApi('notes/show', { noteId: id.value }); }, { immediate: true, }); diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 8c4696b04..ee17be3c4 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -70,11 +70,12 @@ import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFile } from '@/scripts/select-file.js'; -import { mainRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i } from '@/account.js'; +import { mainRouter } from '@/global/router/main.js'; const props = defineProps<{ initPageId?: string; @@ -105,7 +106,7 @@ watch(eyeCatchingImageId, async () => { if (eyeCatchingImageId.value == null) { eyeCatchingImage.value = null; } else { - eyeCatchingImage.value = await os.api('drive/files/show', { + eyeCatchingImage.value = await misskeyApi('drive/files/show', { fileId: eyeCatchingImageId.value, }); } @@ -148,7 +149,7 @@ function save() { if (pageId.value) { options.pageId = pageId.value; - os.api('pages/update', options) + misskeyApi('pages/update', options) .then(page => { currentName.value = name.value.trim(); os.alert({ @@ -157,7 +158,7 @@ function save() { }); }).catch(onError); } else { - os.api('pages/create', options) + misskeyApi('pages/create', options) .then(created => { pageId.value = created.id; currentName.value = name.value.trim(); @@ -176,7 +177,7 @@ function del() { text: i18n.t('removeAreYouSure', { x: title.value.trim() }), }).then(({ canceled }) => { if (canceled) return; - os.api('pages/delete', { + misskeyApi('pages/delete', { pageId: pageId.value, }).then(() => { os.alert({ @@ -191,7 +192,7 @@ function del() { function duplicate() { title.value = title.value + ' - copy'; name.value = name.value + '-copy'; - os.api('pages/create', getSaveOptions()).then(created => { + misskeyApi('pages/create', getSaveOptions()).then(created => { pageId.value = created.id; currentName.value = name.value.trim(); os.alert({ @@ -235,11 +236,11 @@ function removeEyeCatchingImage() { async function init() { if (props.initPageId) { - page.value = await os.api('pages/show', { + page.value = await misskeyApi('pages/show', { pageId: props.initPageId, }); } else if (props.initPageName && props.initUser) { - page.value = await os.api('pages/show', { + page.value = await misskeyApi('pages/show', { name: props.initPageName, username: props.initUser, }); diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 6b06da9a2..d6f25235e 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -81,6 +81,7 @@ import * as Misskey from 'misskey-js'; import XPage from '@/components/page/page.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { url } from '@/config.js'; import MkMediaImage from '@/components/MkMediaImage.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; @@ -113,7 +114,7 @@ const path = computed(() => props.username + '/' + props.pageName); function fetchPage() { page.value = null; - os.api('pages/show', { + misskeyApi('pages/show', { name: props.pageName, username: props.username, }).then(async _page => { diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index a7ca433ed..033ad1a48 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -40,9 +40,9 @@ import { computed, ref } from 'vue'; import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index 95aa64f8d..29df0de30 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, computed, ref } from 'vue'; import JSON5 from 'json5'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import FormLink from '@/components/form/link.vue'; @@ -54,7 +55,7 @@ const scope = computed(() => props.path ? props.path.split('/') : []); const keys = ref(null); function fetchKeys() { - os.api('i/registry/keys-with-type', { + misskeyApi('i/registry/keys-with-type', { scope: scope.value, domain: props.domain === '@' ? null : props.domain, }).then(res => { diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index fb3cc4a55..444aa1511 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, computed, ref } from 'vue'; import JSON5 from 'json5'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; @@ -68,7 +69,7 @@ const value = ref(null); const valueForEditor = ref(null); function fetchValue() { - os.api('i/registry/get-detail', { + misskeyApi('i/registry/get-detail', { scope: scope.value, key: key.value, domain: props.domain === '@' ? null : props.domain, diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 7d1dd751a..06c36456e 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -26,6 +26,7 @@ import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import JSON5 from 'json5'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import FormLink from '@/components/form/link.vue'; @@ -35,7 +36,7 @@ import MkButton from '@/components/MkButton.vue'; const scopesWithDomain = ref(null); function fetchScopes() { - os.api('i/registry/scopes-with-domain').then(res => { + misskeyApi('i/registry/scopes-with-domain').then(res => { scopesWithDomain.value = res; }); } diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index 1aed57724..6263c9a46 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -25,8 +25,8 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { mainRouter } from '@/global/router/main.js'; const props = defineProps<{ token?: string; diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 6dce4f187..631182bbc 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 601479b73..5e6c728d5 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -51,6 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ref, watch } from 'vue'; import tinycolor from 'tinycolor2'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkPagination from '@/components/MkPagination.vue'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import { i18n } from '@/i18n.js'; @@ -94,7 +95,7 @@ watch(sortModeSelect, () => { function fetchDriveInfo(): void { fetching.value = true; - os.api('drive').then(info => { + misskeyApi('drive').then(info => { capacity.value = info.capacity; usage.value = info.usage; fetching.value = false; diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 166e49ac5..89e921f6c 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -62,12 +62,15 @@ import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import bytes from '@/filters/bytes.js'; import { defaultStore } from '@/store.js'; import MkChart from '@/components/MkChart.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; + +const $i = signinRequired(); const fetching = ref(true); const usage = ref(null); @@ -76,6 +79,7 @@ const uploadFolder = ref(null); const alwaysMarkNsfw = ref($i.alwaysMarkNsfw); const meterStyle = computed(() => { + if (!capacity.value || !usage.value) return {}; return { width: `${usage.value / capacity.value * 100}%`, background: tinycolor({ @@ -88,14 +92,14 @@ const meterStyle = computed(() => { const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading')); -os.api('drive').then(info => { +misskeyApi('drive').then(info => { capacity.value = info.capacity; usage.value = info.usage; fetching.value = false; }); if (defaultStore.state.uploadFolder) { - os.api('drive/folders/show', { + misskeyApi('drive/folders/show', { folderId: defaultStore.state.uploadFolder, }).then(response => { uploadFolder.value = response; @@ -107,7 +111,7 @@ function chooseUploadFolder() { defaultStore.set('uploadFolder', folder ? folder.id : null); os.success(); if (defaultStore.state.uploadFolder) { - uploadFolder.value = await os.api('drive/folders/show', { + uploadFolder.value = await misskeyApi('drive/folders/show', { folderId: defaultStore.state.uploadFolder, }); } else { @@ -117,7 +121,7 @@ function chooseUploadFolder() { } function saveProfile() { - os.api('i/update', { + misskeyApi('i/update', { alwaysMarkNsfw: !!alwaysMarkNsfw.value, }).catch(err => { os.alert({ diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue index 003501f45..ea2ab07a8 100644 --- a/packages/frontend/src/pages/settings/email.vue +++ b/packages/frontend/src/pages/settings/email.vue @@ -54,15 +54,18 @@ import MkInfo from '@/components/MkInfo.vue'; import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { instance } from '@/instance.js'; -const emailAddress = ref($i!.email); +const $i = signinRequired(); + +const emailAddress = ref($i.email); const onChangeReceiveAnnouncementEmail = (v) => { - os.api('i/update', { + misskeyApi('i/update', { receiveAnnouncementEmail: v, }); }; @@ -78,14 +81,14 @@ async function saveEmailAddress() { }); } -const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention')); -const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply')); -const emailNotification_quote = ref($i!.emailNotificationTypes.includes('quote')); -const emailNotification_follow = ref($i!.emailNotificationTypes.includes('follow')); -const emailNotification_receiveFollowRequest = ref($i!.emailNotificationTypes.includes('receiveFollowRequest')); +const emailNotification_mention = ref($i.emailNotificationTypes.includes('mention')); +const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply')); +const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote')); +const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow')); +const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest')); const saveNotificationSettings = () => { - os.api('i/update', { + misskeyApi('i/update', { emailNotificationTypes: [ ...[emailNotification_mention.value ? 'mention' : null], ...[emailNotification_reply.value ? 'reply' : null], diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue index 1a07790e7..40bb823ac 100644 --- a/packages/frontend/src/pages/settings/emoji-picker.vue +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -38,15 +38,15 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.preview }} - {{ i18n.ts.default }} - {{ i18n.ts.overwriteFromPinnedEmojis }} + {{ i18n.ts.preview }} + {{ i18n.ts.default }} + {{ i18n.ts.overwriteFromPinnedEmojis }}
- + @@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -78,13 +78,24 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.preview }} - {{ i18n.ts.default }} - {{ i18n.ts.overwriteFromPinnedEmojisForReaction }} + {{ i18n.ts.preview }} + {{ i18n.ts.default }} + {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}
+ + + + + {{ i18n.ts.notSet }} +
+ Change + Reset +
+
+ @@ -128,6 +139,7 @@ import Sortable from 'vuedraggable'; import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; +import FromSlot from '@/components/form/slot.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -136,6 +148,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { deepClone } from '@/scripts/clone.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; import MkEmoji from '@/components/global/MkEmoji.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -152,6 +165,8 @@ const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev); const setDefaultReaction = () => setDefault(pinnedEmojisForReaction); +const like = computed(defaultStore.makeGetterSetter('like')); + const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev); const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev); const setDefaultEmoji = () => setDefault(pinnedEmojis); @@ -220,6 +235,30 @@ async function pickEmoji(itemsRef: Ref, ev: MouseEvent) { }); } +async function reloadAsk() { + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.reloadToApplySetting, + }); + if (canceled) return; + + unisonReload(); +} + +function chooseNewLike(ev: MouseEvent) { + os.pickEmoji(getHTMLElement(ev), { + showPinned: false, + }).then(async emoji => { + defaultStore.set('like', emoji as string); + await reloadAsk(); + }); +} + +async function resetLike() { + defaultStore.set('like', null); + await reloadAsk(); +} + function getHTMLElement(ev: MouseEvent): HTMLElement { const target = ev.currentTarget ?? ev.target; return target as HTMLElement; @@ -239,7 +278,7 @@ watch(pinnedEmojis, () => { definePageMetadata({ title: i18n.ts.emojiPicker, - icon: 'ti ti-mood-happy', + icon: 'ph-smiley ph-bold ph-lg', }); diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 8eacdd32e..b4e368021 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -146,6 +146,7 @@ SPDX-License-Identifier: AGPL-3.0-only +
@@ -190,6 +191,23 @@ SPDX-License-Identifier: AGPL-3.0-only + + +
+ + {{ i18n.ts.showVisibilitySelectorOnBoost }} + + + + + + + + + +
+
+ @@ -257,6 +275,7 @@ import MkInfo from '@/components/MkInfo.vue'; import { langs } from '@/config.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -326,6 +345,8 @@ const noteDesign = computed(defaultStore.makeGetterSetter('noteDesign')); const uncollapseCW = computed(defaultStore.makeGetterSetter('uncollapseCW')); const expandLongNote = computed(defaultStore.makeGetterSetter('expandLongNote')); const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect')); +const showVisibilitySelectorOnBoost = computed(defaultStore.makeGetterSetter('showVisibilitySelectorOnBoost')); +const visibilityOnBoost = computed(defaultStore.makeGetterSetter('visibilityOnBoost')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -381,6 +402,8 @@ watch([ keepScreenOn, disableStreamingTimeline, enableSeasonalScreenEffect, + showVisibilitySelectorOnBoost, + visibilityOnBoost, ], async () => { await reloadAsk(); }); @@ -416,7 +439,7 @@ function removeEmojiIndex(lang: string) { } async function setPinnedList() { - const lists = await os.api('users/lists/list'); + const lists = await misskeyApi('users/lists/list'); const { canceled, result: list } = await os.select({ title: i18n.ts.selectList, items: lists.map(x => ({ diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 7ca1faf40..ee21134d5 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -36,6 +36,14 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.export }}
+ + + + + + {{ i18n.ts.export }} + +
@@ -133,11 +141,12 @@ import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFile } from '@/scripts/select-file.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i } from '@/account.js'; -import { defaultStore } from "@/store.js"; +import { defaultStore } from '@/store.js'; const excludeMutingUsers = ref(false); const excludeInactiveUsers = ref(false); @@ -166,15 +175,19 @@ const onError = (ev) => { }; const exportNotes = () => { - os.api('i/export-notes', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-notes', {}).then(onExportSuccess).catch(onError); }; const exportFavorites = () => { - os.api('i/export-favorites', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-favorites', {}).then(onExportSuccess).catch(onError); +}; + +const exportClips = () => { + misskeyApi('i/export-clips', {}).then(onExportSuccess).catch(onError); }; const exportFollowing = () => { - os.api('i/export-following', { + misskeyApi('i/export-following', { excludeMuting: excludeMutingUsers.value, excludeInactive: excludeInactiveUsers.value, }) @@ -182,24 +195,24 @@ const exportFollowing = () => { }; const exportBlocking = () => { - os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-blocking', {}).then(onExportSuccess).catch(onError); }; const exportUserLists = () => { - os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-user-lists', {}).then(onExportSuccess).catch(onError); }; const exportMuting = () => { - os.api('i/export-mute', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-mute', {}).then(onExportSuccess).catch(onError); }; const exportAntennas = () => { - os.api('i/export-antennas', {}).then(onExportSuccess).catch(onError); + misskeyApi('i/export-antennas', {}).then(onExportSuccess).catch(onError); }; const importFollowing = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-following', { + misskeyApi('i/import-following', { fileId: file.id, withReplies: withReplies.value, }).then(onImportSuccess).catch(onError); @@ -207,7 +220,7 @@ const importFollowing = async (ev) => { const importNotes = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-notes', { + misskeyApi('i/import-notes', { fileId: file.id, type: noteType.value, }).then(onImportSuccess).catch(onError); @@ -215,22 +228,22 @@ const importNotes = async (ev) => { const importUserLists = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); + misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importMuting = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); + misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importBlocking = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); + misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importAntennas = async (ev) => { const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError); + misskeyApi('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 96575e097..bcdce7bb6 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -34,9 +34,9 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue'; import { signout, $i } from '@/account.js'; import { clearCache } from '@/scripts/clear-cache.js'; import { instance } from '@/instance.js'; -import { useRouter } from '@/router.js'; import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import * as os from '@/os.js'; +import { useRouter } from '@/global/router/supplier.js'; const indexInfo = { title: i18n.ts.settings, diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue index 3b47189eb..a0f27f356 100644 --- a/packages/frontend/src/pages/settings/migration.vue +++ b/packages/frontend/src/pages/settings/migration.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + @@ -66,24 +66,27 @@ import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkUserInfo from '@/components/MkUserInfo.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { unisonReload } from '@/scripts/unison-reload.js'; +const $i = signinRequired(); + const moveToAccount = ref(''); const movedTo = ref(); const accountAliases = ref(['']); async function init() { - if ($i?.movedTo) { - movedTo.value = await os.api('users/show', { userId: $i.movedTo }); + if ($i.movedTo) { + movedTo.value = await misskeyApi('users/show', { userId: $i.movedTo }); } else { moveToAccount.value = ''; } - if ($i?.alsoKnownAs && $i.alsoKnownAs.length > 0) { - const alsoKnownAs = await os.api('users/show', { userIds: $i.alsoKnownAs }); + if ($i.alsoKnownAs && $i.alsoKnownAs.length > 0) { + const alsoKnownAs = await misskeyApi('users/show', { userIds: $i.alsoKnownAs }); accountAliases.value = (alsoKnownAs && alsoKnownAs.length > 0) ? alsoKnownAs.map(user => `@${Misskey.acct.toString(user)}`) : ['']; } else { accountAliases.value = ['']; diff --git a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue index 0e149fd46..a5b7f7a79 100644 --- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue @@ -19,11 +19,13 @@ import { ref, watch } from 'vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -const instanceMutes = ref($i!.mutedInstances.join('\n')); +const $i = signinRequired(); + +const instanceMutes = ref($i.mutedInstances.join('\n')); const changed = ref(false); async function save() { @@ -32,7 +34,7 @@ async function save() { .map(el => el.trim()) .filter(el => el); - await os.api('i/update', { + await misskeyApi('i/update', { mutedInstances: mutes, }); diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index a996a03cc..2535ab634 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -9,14 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only - + - + @@ -136,9 +136,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import * as os from '@/os.js'; import { infoImageUrl } from '@/instance.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import MkFolder from '@/components/MkFolder.vue'; +const $i = signinRequired(); + const renoteMutingPagination = { endpoint: 'renote-mute/list' as const, limit: 10, diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 0bdfbdf74..d257713e9 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -62,18 +62,21 @@ import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { notificationTypes } from '@/const.js'; +const $i = signinRequired(); + const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned']; const allowButton = shallowRef>(); const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer); const sendReadMessage = computed(() => pushRegistrationInServer.value?.sendReadMessage || false); -const userLists = await os.api('users/lists/list'); +const userLists = await misskeyApi('users/lists/list'); async function readAllUnreadNotes() { await os.apiWithDialog('i/read-all-unread-notes'); @@ -86,11 +89,11 @@ async function readAllNotifications() { async function updateReceiveConfig(type, value) { await os.apiWithDialog('i/update', { notificationRecieveConfig: { - ...$i!.notificationRecieveConfig, + ...$i.notificationRecieveConfig, [type]: value, }, }).then(i => { - $i!.notificationRecieveConfig = i.notificationRecieveConfig; + $i.notificationRecieveConfig = i.notificationRecieveConfig; }); } @@ -107,7 +110,7 @@ function onChangeSendReadMessage(v: boolean) { } function testNotification(): void { - os.api('notifications/test-notification'); + misskeyApi('notifications/test-notification'); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index efda0c00b..8c7a62c6f 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -104,26 +104,21 @@ import FormInfo from '@/components/MkInfo.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; -import { signout, $i } from '@/account.js'; +import { signout, signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import FormSection from '@/components/form/section.vue'; +const $i = signinRequired(); + const reportError = computed(defaultStore.makeGetterSetter('reportError')); const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct')); const devMode = computed(defaultStore.makeGetterSetter('devMode')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); -function onChangeInjectFeaturedNote(v) { - os.api('i/update', { - injectFeaturedNote: v, - }).then((i) => { - $i!.injectFeaturedNote = i.injectFeaturedNote; - }); -} - async function deleteAccount() { { const { canceled } = await os.confirm({ @@ -165,11 +160,11 @@ async function updateRepliesAll(withReplies: boolean) { }); if (canceled) return; - os.api('following/update-all', { withReplies }); + misskeyApi('following/update-all', { withReplies }); } const exportData = () => { - os.api('i/export-data', {}).then(() => { + misskeyApi('i/export-data', {}).then(() => { os.alert({ type: 'info', text: i18n.ts.exportRequested, diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index c7538f3a1..5fccf15df 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -43,6 +43,7 @@ import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { useStream } from '@/stream.js'; @@ -146,7 +147,7 @@ const connection = $i && useStream().useChannel('main'); const profiles = ref | null>(null); -os.api('i/registry/get-all', { scope }) +misskeyApi('i/registry/get-all', { scope }) .then(res => { profiles.value = res || {}; }); diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 62056ff8a..7dcab1acf 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -77,12 +77,14 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSection from '@/components/form/section.vue'; import MkFolder from '@/components/MkFolder.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +const $i = signinRequired(); + const isLocked = ref($i.isLocked); const autoAcceptFollowed = ref($i.autoAcceptFollowed); const noCrawle = ref($i.noCrawle); @@ -90,8 +92,8 @@ const noindex = ref($i.noindex); const isExplorable = ref($i.isExplorable); const hideOnlineStatus = ref($i.hideOnlineStatus); const publicReactions = ref($i.publicReactions); -const followingVisibility = ref($i?.followingVisibility); -const followersVisibility = ref($i?.followersVisibility); +const followingVisibility = ref($i.followingVisibility); +const followersVisibility = ref($i.followersVisibility); const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); @@ -99,7 +101,7 @@ const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberN const keepCw = computed(defaultStore.makeGetterSetter('keepCw')); function save() { - os.api('i/update', { + misskeyApi('i/update', { isLocked: !!isLocked.value, autoAcceptFollowed: !!autoAcceptFollowed.value, noCrawle: !!noCrawle.value, diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 4bae635d0..ce430d97e 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -127,7 +127,7 @@ import FormSlot from '@/components/form/slot.vue'; import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { langmap } from '@/scripts/langmap.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement } from '@/scripts/achievements.js'; @@ -135,6 +135,8 @@ import { defaultStore } from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +const $i = signinRequired(); + const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance')); @@ -152,10 +154,10 @@ const profile = reactive({ description: $i.description, location: $i.location, birthday: $i.birthday, - listenbrainz: $i?.listenbrainz, + listenbrainz: $i.listenbrainz, lang: $i.lang, - isBot: $i.isBot, - isCat: $i.isCat, + isBot: $i.isBot ?? false, + isCat: $i.isCat ?? false, speakAsCat: $i.speakAsCat, }); @@ -165,7 +167,7 @@ watch(() => profile, () => { deep: true, }); -const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []); +const fields = ref($i.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []); const fieldEditMode = ref(false); function addField() { @@ -251,53 +253,131 @@ function changeAvatar(ev) { } function changeBanner(ev) { - selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => { - let originalOrCropped = file; + if ($i.bannerId) { + os.popupMenu([{ + text: i18n.ts._profile.updateBanner, + action: async () => { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => { + let originalOrCropped = file; - const { canceled } = await os.confirm({ - type: 'question', - text: i18n.t('cropImageAsk'), - okText: i18n.ts.cropYes, - cancelText: i18n.ts.cropNo, - }); + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('cropImageAsk'), + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, + }); - if (!canceled) { - originalOrCropped = await os.cropImage(file, { - aspectRatio: 2, + if (!canceled) { + originalOrCropped = await os.cropImage(file, { + aspectRatio: 2, + }); + } + + const i = await os.apiWithDialog('i/update', { + bannerId: originalOrCropped.id, + }); + $i.bannerId = i.bannerId; + $i.bannerUrl = i.bannerUrl; + }); + }, + }, { + text: i18n.ts._profile.removeBanner, + action: async () => { + const i = await os.apiWithDialog('i/update', { + bannerId: null, + }); + $i.bannerId = i.bannerId; + $i.bannerUrl = i.bannerUrl; + }, + }], ev.currentTarget ?? ev.target); + } else { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => { + let originalOrCropped = file; + + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('cropImageAsk'), + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, }); - } - const i = await os.apiWithDialog('i/update', { - bannerId: originalOrCropped.id, + if (!canceled) { + originalOrCropped = await os.cropImage(file, { + aspectRatio: 2, + }); + } + + const i = await os.apiWithDialog('i/update', { + bannerId: originalOrCropped.id, + }); + $i.bannerId = i.bannerId; + $i.bannerUrl = i.bannerUrl; }); - $i.bannerId = i.bannerId; - $i.bannerUrl = i.bannerUrl; - }); + } } function changeBackground(ev) { - selectFile(ev.currentTarget ?? ev.target, i18n.ts.background).then(async (file) => { - let originalOrCropped = file; + if ($i.backgroundId) { + os.popupMenu([{ + text: i18n.ts._profile.updateBackground, + action: async () => { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.background).then(async (file) => { + let originalOrCropped = file; - const { canceled } = await os.confirm({ - type: 'question', - text: i18n.t('cropImageAsk'), - okText: i18n.ts.cropYes, - cancelText: i18n.ts.cropNo, - }); + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('cropImageAsk'), + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, + }); - if (!canceled) { - originalOrCropped = await os.cropImage(file, { - aspectRatio: 1, + if (!canceled) { + originalOrCropped = await os.cropImage(file, { + aspectRatio: 1, + }); + } + + const i = await os.apiWithDialog('i/update', { + backgroundId: originalOrCropped.id, + }); + $i.backgroundId = i.backgroundId; + $i.backgroundUrl = i.backgroundUrl; + }); + }, + }, { + text: i18n.ts._profile.removeBackground, + action: async () => { + const i = await os.apiWithDialog('i/update', { + backgroundId: null, + }); + $i.backgroundId = i.backgroundId; + $i.backgroundUrl = i.backgroundUrl; + }, + }], ev.currentTarget ?? ev.target); + } else { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.background).then(async (file) => { + let originalOrCropped = file; + + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('cropImageAsk'), + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, }); - } - const i = await os.apiWithDialog('i/update', { - backgroundId: originalOrCropped.id, + if (!canceled) { + originalOrCropped = await os.cropImage(file, { + aspectRatio: 1, + }); + } + + const i = await os.apiWithDialog('i/update', { + backgroundId: originalOrCropped.id, + }); + $i.backgroundId = i.backgroundId; + $i.backgroundUrl = i.backgroundUrl; }); - $i.backgroundId = i.backgroundId; - $i.backgroundUrl = i.backgroundUrl; - }); + } } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue index 716b168c9..cf6e75cba 100644 --- a/packages/frontend/src/pages/settings/roles.vue +++ b/packages/frontend/src/pages/settings/roles.vue @@ -27,15 +27,11 @@ import { computed } from 'vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { signinRequired } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; -function save() { - os.apiWithDialog('i/update', { - - }); -} +const $i = signinRequired(); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index 9ae479e6e..44f98969a 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -47,6 +47,7 @@ import FormSlot from '@/components/form/slot.vue'; import MkButton from '@/components/MkButton.vue'; import MkPagination from '@/components/MkPagination.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -92,7 +93,7 @@ async function regenerateToken() { const auth = await os.authenticateDialog(); if (auth.canceled) return; - os.api('i/regenerate-token', { + misskeyApi('i/regenerate-token', { password: auth.result.password, token: auth.result.token, }); diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue index a43ffb1f0..73812d592 100644 --- a/packages/frontend/src/pages/settings/sounds.sound.vue +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -32,7 +32,8 @@ import MkButton from '@/components/MkButton.vue'; import MkRange from '@/components/MkRange.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js'; import { selectFile } from '@/scripts/select-file.js'; const props = defineProps<{ @@ -53,7 +54,7 @@ const fileName = ref(''); const volume = ref(props.volume); if (type.value === '_driveFile_' && fileId.value) { - const apiRes = await os.api('drive/files/show', { + const apiRes = await misskeyApi('drive/files/show', { fileId: fileId.value, }); fileName.value = apiRes.name; @@ -118,7 +119,7 @@ function listen() { return; } - playFile(type.value === '_driveFile_' ? { + playMisskeySfxFile(type.value === '_driveFile_' ? { type: '_driveFile_', fileId: fileId.value as string, fileUrl: fileUrl.value as string, diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue index c45e386ac..4270ef285 100644 --- a/packages/frontend/src/pages/settings/statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.vue @@ -21,7 +21,7 @@ import { v4 as uuid } from 'uuid'; import XStatusbar from './statusbar.statusbar.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -31,7 +31,7 @@ const statusbars = defaultStore.reactiveState.statusbars; const userLists = ref(null); onMounted(() => { - os.api('users/lists/list').then(res => { + misskeyApi('users/lists/list').then(res => { userLists.value = res; }); }); diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index f6e2f6331..7dd995fc8 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -48,9 +48,10 @@ import FormSection from '@/components/form/section.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { useRouter } from '@/router.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -58,7 +59,7 @@ const props = defineProps<{ webhookId: string; }>(); -const webhook = await os.api('i/webhooks/show', { +const webhook = await misskeyApi('i/webhooks/show', { webhookId: props.webhookId, }); diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index a978be0ae..e2d3362fb 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -37,6 +37,7 @@ import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { postMessageToParentWindow } from '@/scripts/post-message.js'; import { i18n } from '@/i18n.js'; @@ -76,7 +77,7 @@ async function init() { ] // TypeScriptの指示通りに変換する .map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q) - .map(q => os.api('users/show', q) + .map(q => misskeyApi('users/show', q) .then(user => { visibleUsers.value.push(user); }, () => { @@ -91,11 +92,11 @@ async function init() { const replyId = urlParams.get('replyId'); const replyUri = urlParams.get('replyUri'); if (replyId) { - reply.value = await os.api('notes/show', { + reply.value = await misskeyApi('notes/show', { noteId: replyId, }); } else if (replyUri) { - const obj = await os.api('ap/show', { + const obj = await misskeyApi('ap/show', { uri: replyUri, }); if (obj.type === 'Note') { @@ -108,11 +109,11 @@ async function init() { const renoteId = urlParams.get('renoteId'); const renoteUri = urlParams.get('renoteUri'); if (renoteId) { - renote.value = await os.api('notes/show', { + renote.value = await misskeyApi('notes/show', { noteId: renoteId, }); } else if (renoteUri) { - const obj = await os.api('ap/show', { + const obj = await misskeyApi('ap/show', { uri: renoteUri, }); if (obj.type === 'Note') { @@ -126,7 +127,7 @@ async function init() { if (fileIds) { await Promise.all( fileIds.split(',') - .map(fileId => os.api('drive/files/show', { fileId }) + .map(fileId => misskeyApi('drive/files/show', { fileId }) .then(file => { files.value.push(file); }, () => { diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 4009652bc..0c2ad9102 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -31,6 +31,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; const submitting = ref(false); @@ -42,7 +43,7 @@ function submit() { if (submitting.value) return; submitting.value = true; - os.api('signup-pending', { + misskeyApi('signup-pending', { code: props.code, }).then(res => { if (res.pendingApproval) { diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index f9adee94d..f92cc3dfc 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -41,6 +41,7 @@ import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import { scroll } from '@/scripts/scroll.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; @@ -96,7 +97,7 @@ async function chooseList(ev: MouseEvent): Promise { (lists.length === 0 ? undefined : { type: 'divider' }), { type: 'link' as const, - icon: 'ti ti-plus', + icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.createNew, to: '/my/lists', }, @@ -116,7 +117,7 @@ async function chooseAntenna(ev: MouseEvent): Promise { (antennas.length === 0 ? undefined : { type: 'divider' }), { type: 'link' as const, - icon: 'ti ti-plus', + icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.createNew, to: '/my/antennas', }, @@ -125,7 +126,7 @@ async function chooseAntenna(ev: MouseEvent): Promise { } async function chooseChannel(ev: MouseEvent): Promise { - const channels = await os.api('channels/my-favorites', { + const channels = await misskeyApi('channels/my-favorites', { limit: 100, }); const items: MenuItem[] = [ @@ -143,7 +144,7 @@ async function chooseChannel(ev: MouseEvent): Promise { (channels.length === 0 ? undefined : { type: 'divider' }), { type: 'link' as const, - icon: 'ti ti-plus', + icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.createNew, to: '/channels', }, diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index 3ec23df7b..fcff94e10 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -28,10 +28,10 @@ import { computed, watch, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; import { scroll } from '@/scripts/scroll.js'; -import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; +import { useRouter } from '@/global/router/supplier.js'; const router = useRouter(); @@ -45,7 +45,7 @@ const tlEl = shallowRef>(); const rootEl = shallowRef(); watch(() => props.listId, async () => { - list.value = await os.api('users/lists/show', { + list.value = await misskeyApi('users/lists/show', { listId: props.listId, }); }, { immediate: true }); diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue index bd1159cb3..798b64064 100644 --- a/packages/frontend/src/pages/user/activity.following.vue +++ b/packages/frontend/src/pages/user/activity.following.vue @@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -61,7 +61,7 @@ async function renderChart() { })); }; - const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue index ff46db965..ea3276a89 100644 --- a/packages/frontend/src/pages/user/activity.heatmap.vue +++ b/packages/frontend/src/pages/user/activity.heatmap.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { alpha } from '@/scripts/color.js'; @@ -74,7 +74,7 @@ async function renderChart() { let values; if (props.src === 'notes') { - const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); values = raw.inc; } diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue index dd035641d..a55d98d98 100644 --- a/packages/frontend/src/pages/user/activity.notes.vue +++ b/packages/frontend/src/pages/user/activity.notes.vue @@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -61,7 +61,7 @@ async function renderChart() { })); }; - const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue index 2dd9a1570..fe9acd322 100644 --- a/packages/frontend/src/pages/user/activity.pv.vue +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue'; import { Chart, ChartDataset } from 'chart.js'; import * as Misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -61,7 +61,7 @@ async function renderChart() { })); }; - const raw = await os.api('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' }); + const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' }); const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue index 36f1b4543..4a898f1ee 100644 --- a/packages/frontend/src/pages/user/followers.vue +++ b/packages/frontend/src/pages/user/followers.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XFollowList from './follow-list.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; @@ -37,7 +37,7 @@ const error = ref(null); function fetchUser(): void { if (props.acct == null) return; user.value = null; - os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { + misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => { user.value = u; }).catch(err => { error.value = err; diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue index 43876b77c..bfa962f25 100644 --- a/packages/frontend/src/pages/user/following.vue +++ b/packages/frontend/src/pages/user/following.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XFollowList from './follow-list.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; @@ -37,7 +37,7 @@ const error = ref(null); function fetchUser(): void { if (props.acct == null) return; user.value = null; - os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { + misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => { user.value = u; }).catch(err => { error.value = err; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 44a8ca250..ed7dabce6 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -185,14 +185,14 @@ import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; -import { useRouter } from '@/router.js'; import { i18n } from '@/i18n.js'; import { $i, iAmModerator } from '@/account.js'; import { dateString } from '@/filters/date.js'; import { confetti } from '@/scripts/confetti.js'; -import { api } from '@/os.js'; import { defaultStore } from '@/store.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { useRouter } from '@/global/router/supplier.js'; function calcAge(birthdate: string): number { const date = new Date(birthdate); @@ -262,7 +262,7 @@ const background = computed(() => { }); watch(moderationNote, async () => { - await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); + await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); }); const pagination = { @@ -279,7 +279,7 @@ const AllPagination = { params: computed(() => ({ userId: props.user.id, withRenotes: noteview.value === 'all', - withReplies: noteview.value === 'all' || noteview.value === 'files', + withReplies: noteview.value === 'all', withChannelNotes: noteview.value === 'all', withFiles: noteview.value === 'files', })), @@ -333,7 +333,7 @@ function adjustMemoTextarea() { } async function updateMemo() { - await api('users/update-memo', { + await misskeyApi('users/update-memo', { memo: memoDraft.value, userId: props.user.id, }); diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue index 30817db77..315999ee8 100644 --- a/packages/frontend/src/pages/user/index.files.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -37,7 +37,7 @@ import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { notePage } from '@/filters/note.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import { defaultStore } from '@/store.js'; @@ -61,7 +61,7 @@ function thumbnail(image: Misskey.entities.DriveFile): string { } onMounted(() => { - os.api('users/notes', { + misskeyApi('users/notes', { userId: props.user.id, withFiles: true, limit: 15, diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 44b4f84ca..dea5fb7da 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { acct as getAcct } from '@/filters/user.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -63,7 +63,7 @@ const error = ref(null); function fetchUser(): void { if (props.acct == null) return; user.value = null; - os.api('users/show', Misskey.acct.parse(props.acct)).then(u => { + misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => { user.value = u; }).catch(err => { error.value = err; diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 50f86a0ae..f4c86da6a 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -39,7 +39,7 @@ import XTimeline from './welcome.timeline.vue'; import MarqueeText from '@/components/MkMarquee.vue'; import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue'; import misskeysvg from '/client-assets/sharkey.svg'; -import * as os from '@/os.js'; +import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; @@ -53,11 +53,11 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string return getProxiedImageUrl(instance.iconUrl, 'preview'); } -os.api('meta', { detail: true }).then(_meta => { +misskeyApi('meta', { detail: true }).then(_meta => { meta.value = _meta; }); -os.apiGet('federation/instances', { +misskeyApiGet('federation/instances', { sort: '+pubSub', limit: 20, }).then(_instances => { diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index c2f9d4e58..9a2719897 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -40,6 +40,7 @@ import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import { host, version } from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; import MkAnimBg from '@/components/MkAnimBg.vue'; @@ -52,7 +53,7 @@ function submit() { if (submitting.value) return; submitting.value = true; - os.api('admin/accounts/create', { + misskeyApi('admin/accounts/create', { username: username.value, password: password.value, }).then(res => { diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 2cbe0ed9b..228f5a13b 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -32,14 +32,14 @@ import { onUpdated, ref, shallowRef } from 'vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; import MkMediaList from '@/components/MkMediaList.vue'; import MkPoll from '@/components/MkPoll.vue'; -import * as os from '@/os.js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { getScrollContainer } from '@/scripts/scroll.js'; const notes = ref([]); const isScrolling = ref(false); const scrollEl = shallowRef(); -os.apiGet('notes/featured').then(_notes => { +misskeyApiGet('notes/featured').then(_notes => { notes.value = _notes; }); diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue index 7f0af1b83..225ab9151 100644 --- a/packages/frontend/src/pages/welcome.vue +++ b/packages/frontend/src/pages/welcome.vue @@ -16,12 +16,12 @@ import * as Misskey from 'misskey-js'; import XSetup from './welcome.setup.vue'; import XEntrance from './welcome.entrance.a.vue'; import { instanceName } from '@/config.js'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; const meta = ref(null); -os.api('meta', { detail: true }).then(res => { +misskeyApi('meta', { detail: true }).then(res => { meta.value = res; }); diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index b2254a061..8723110b0 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -8,7 +8,7 @@ import { onUnmounted, Ref, ref, watch } from 'vue'; import { BroadcastChannel } from 'broadcast-channel'; import { $i } from '@/account.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { get, set } from '@/scripts/idb-proxy.js'; import { defaultStore } from '@/store.js'; import { useStream } from '@/stream.js'; @@ -134,7 +134,7 @@ export class Storage { window.setTimeout(async () => { await defaultStore.ready; - api('i/registry/get-all', { scope: ['client', this.key] }) + misskeyApi('i/registry/get-all', { scope: ['client', this.key] }) .then(kvs => { const cache: Partial = {}; for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { @@ -168,7 +168,7 @@ export class Storage { this.reactiveState[key].value = this.state[key] = rawValue; return this.addIdbSetJob(async () => { - if (_DEV_) console.log(`set ${key} start`); + if (_DEV_) console.log(`set ${String(key)} start`); switch (this.def[key].where) { case 'device': { this.pizzaxChannel.postMessage({ @@ -199,7 +199,7 @@ export class Storage { const cache = await get(this.registryCacheKeyName) || {}; cache[key] = rawValue; await set(this.registryCacheKeyName, cache); - await api('i/registry/set', { + await misskeyApi('i/registry/set', { scope: ['client', this.key], key: key.toString(), value: rawValue, @@ -207,7 +207,7 @@ export class Storage { break; } } - if (_DEV_) console.log(`set ${key} complete`); + if (_DEV_) console.log(`set ${String(key)} complete`); }); } @@ -225,7 +225,10 @@ export class Storage { * 特定のキーの、簡易的なgetter/setterを作ります * 主にvue場で設定コントロールのmodelとして使う用 */ - public makeGetterSetter(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) { + public makeGetterSetter(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): { + get: () => T[K]['default']; + set: (value: T[K]['default']) => void; + } { const valueRef = ref(this.state[key]); const stop = watch(this.reactiveState[key], val => { diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index 5e49af485..acc3e836f 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -4,7 +4,7 @@ */ import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; -import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import { inputText } from '@/os.js'; import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js'; @@ -19,19 +19,7 @@ export async function install(plugin: Plugin): Promise { plugin: plugin, storageKey: 'plugins:' + plugin.id, }), { - in: (q): Promise => { - return new Promise(ok => { - inputText({ - title: q, - }).then(({ canceled, result: a }) => { - if (canceled) { - ok(''); - } else { - ok(a); - } - }); - }); - }, + in: aiScriptReadline, out: (value): void => { console.log(value); }, diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts deleted file mode 100644 index b861afa9a..000000000 --- a/packages/frontend/src/router.ts +++ /dev/null @@ -1,558 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue'; -import { Router } from '@/nirax.js'; -import { $i, iAmModerator } from '@/account.js'; -import MkLoading from '@/pages/_loading_.vue'; -import MkError from '@/pages/_error_.vue'; - -export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ - loader: loader, - loadingComponent: MkLoading, - errorComponent: MkError, -}); - -export const routes = [{ - path: '/@:initUser/pages/:initPageName/view-source', - component: page(() => import('./pages/page-editor/page-editor.vue')), -}, { - path: '/@:username/pages/:pageName', - component: page(() => import('./pages/page.vue')), -}, { - path: '/@:acct/following', - component: page(() => import('./pages/user/following.vue')), -}, { - path: '/@:acct/followers', - component: page(() => import('./pages/user/followers.vue')), -}, { - name: 'user', - path: '/@:acct/:page?', - component: page(() => import('./pages/user/index.vue')), -}, { - name: 'note', - path: '/notes/:noteId', - component: page(() => import('./pages/note.vue')), -}, { - name: 'list', - path: '/list/:listId', - component: page(() => import('./pages/list.vue')), -}, { - path: '/clips/:clipId', - component: page(() => import('./pages/clip.vue')), -}, { - path: '/instance-info/:host', - component: page(() => import('./pages/instance-info.vue')), -}, { - name: 'settings', - path: '/settings', - component: page(() => import('./pages/settings/index.vue')), - loginRequired: true, - children: [{ - path: '/profile', - name: 'profile', - component: page(() => import('./pages/settings/profile.vue')), - }, { - path: '/avatar-decoration', - name: 'avatarDecoration', - component: page(() => import('./pages/settings/avatar-decoration.vue')), - }, { - path: '/roles', - name: 'roles', - component: page(() => import('./pages/settings/roles.vue')), - }, { - path: '/privacy', - name: 'privacy', - component: page(() => import('./pages/settings/privacy.vue')), - }, { - path: '/emoji-picker', - name: 'emojiPicker', - component: page(() => import('./pages/settings/emoji-picker.vue')), - }, { - path: '/drive', - name: 'drive', - component: page(() => import('./pages/settings/drive.vue')), - }, { - path: '/drive/cleaner', - name: 'drive', - component: page(() => import('./pages/settings/drive-cleaner.vue')), - }, { - path: '/notifications', - name: 'notifications', - component: page(() => import('./pages/settings/notifications.vue')), - }, { - path: '/email', - name: 'email', - component: page(() => import('./pages/settings/email.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('./pages/settings/security.vue')), - }, { - path: '/general', - name: 'general', - component: page(() => import('./pages/settings/general.vue')), - }, { - path: '/theme/install', - name: 'theme', - component: page(() => import('./pages/settings/theme.install.vue')), - }, { - path: '/theme/manage', - name: 'theme', - component: page(() => import('./pages/settings/theme.manage.vue')), - }, { - path: '/theme', - name: 'theme', - component: page(() => import('./pages/settings/theme.vue')), - }, { - path: '/navbar', - name: 'navbar', - component: page(() => import('./pages/settings/navbar.vue')), - }, { - path: '/statusbar', - name: 'statusbar', - component: page(() => import('./pages/settings/statusbar.vue')), - }, { - path: '/sounds', - name: 'sounds', - component: page(() => import('./pages/settings/sounds.vue')), - }, { - path: '/plugin/install', - name: 'plugin', - component: page(() => import('./pages/settings/plugin.install.vue')), - }, { - path: '/plugin', - name: 'plugin', - component: page(() => import('./pages/settings/plugin.vue')), - }, { - path: '/import-export', - name: 'import-export', - component: page(() => import('./pages/settings/import-export.vue')), - }, { - path: '/mute-block', - name: 'mute-block', - component: page(() => import('./pages/settings/mute-block.vue')), - }, { - path: '/api', - name: 'api', - component: page(() => import('./pages/settings/api.vue')), - }, { - path: '/apps', - name: 'api', - component: page(() => import('./pages/settings/apps.vue')), - }, { - path: '/webhook/edit/:webhookId', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.edit.vue')), - }, { - path: '/webhook/new', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.new.vue')), - }, { - path: '/webhook', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.vue')), - }, { - path: '/deck', - name: 'deck', - component: page(() => import('./pages/settings/deck.vue')), - }, { - path: '/preferences-backups', - name: 'preferences-backups', - component: page(() => import('./pages/settings/preferences-backups.vue')), - }, { - path: '/migration', - name: 'migration', - component: page(() => import('./pages/settings/migration.vue')), - }, { - path: '/custom-css', - name: 'general', - component: page(() => import('./pages/settings/custom-css.vue')), - }, { - path: '/accounts', - name: 'profile', - component: page(() => import('./pages/settings/accounts.vue')), - }, { - path: '/other', - name: 'other', - component: page(() => import('./pages/settings/other.vue')), - }, { - path: '/', - component: page(() => import('./pages/_empty_.vue')), - }], -}, { - path: '/reset-password/:token?', - component: page(() => import('./pages/reset-password.vue')), -}, { - path: '/signup-complete/:code', - component: page(() => import('./pages/signup-complete.vue')), -}, { - path: '/announcements', - component: page(() => import('./pages/announcements.vue')), -}, { - path: '/about', - component: page(() => import('./pages/about.vue')), - hash: 'initialTab', -}, { - path: '/about-sharkey', - component: page(() => import('./pages/about-sharkey.vue')), -}, { - path: '/invite', - name: 'invite', - component: page(() => import('./pages/invite.vue')), -}, { - path: '/ads', - component: page(() => import('./pages/ads.vue')), -}, { - path: '/theme-editor', - component: page(() => import('./pages/theme-editor.vue')), - loginRequired: true, -}, { - path: '/roles/:role', - component: page(() => import('./pages/role.vue')), -}, { - path: '/user-tags/:tag', - component: page(() => import('./pages/user-tag.vue')), -}, { - path: '/explore', - component: page(() => import('./pages/explore.vue')), - hash: 'initialTab', -}, { - path: '/search', - component: page(() => import('./pages/search.vue')), - query: { - q: 'query', - channel: 'channel', - type: 'type', - origin: 'origin', - }, -}, { - path: '/authorize-follow', - component: page(() => import('./pages/follow.vue')), - loginRequired: true, -}, { - path: '/share', - component: page(() => import('./pages/share.vue')), - loginRequired: true, -}, { - path: '/api-console', - component: page(() => import('./pages/api-console.vue')), - loginRequired: true, -}, { - path: '/scratchpad', - component: page(() => import('./pages/scratchpad.vue')), -}, { - path: '/auth/:token', - component: page(() => import('./pages/auth.vue')), -}, { - path: '/miauth/:session', - component: page(() => import('./pages/miauth.vue')), - query: { - callback: 'callback', - name: 'name', - icon: 'icon', - permission: 'permission', - }, -}, { - path: '/tags/:tag', - component: page(() => import('./pages/tag.vue')), -}, { - path: '/pages/new', - component: page(() => import('./pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages/edit/:initPageId', - component: page(() => import('./pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages', - component: page(() => import('./pages/pages.vue')), -}, { - path: '/play/:id/edit', - component: page(() => import('./pages/flash/flash-edit.vue')), - loginRequired: true, -}, { - path: '/play/new', - component: page(() => import('./pages/flash/flash-edit.vue')), - loginRequired: true, -}, { - path: '/play/:id', - component: page(() => import('./pages/flash/flash.vue')), -}, { - path: '/play', - component: page(() => import('./pages/flash/flash-index.vue')), -}, { - path: '/gallery/:postId/edit', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/new', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/:postId', - component: page(() => import('./pages/gallery/post.vue')), -}, { - path: '/gallery', - component: page(() => import('./pages/gallery/index.vue')), -}, { - path: '/channels/:channelId/edit', - component: page(() => import('./pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/new', - component: page(() => import('./pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/:channelId', - component: page(() => import('./pages/channel.vue')), -}, { - path: '/channels', - component: page(() => import('./pages/channels.vue')), -}, { - path: '/avatar-decorations', - name: 'avatarDecorations', - component: page(() => import('./pages/avatar-decorations.vue')), -}, { - path: '/custom-emojis-manager', - component: page(() => import('./pages/custom-emojis-manager.vue')), -}, { - path: '/registry/keys/:domain/:path(*)?', - component: page(() => import('./pages/registry.keys.vue')), -}, { - path: '/registry/value/:domain/:path(*)?', - component: page(() => import('./pages/registry.value.vue')), -}, { - path: '/registry', - component: page(() => import('./pages/registry.vue')), -}, { - path: '/install-extentions', - component: page(() => import('./pages/install-extentions.vue')), - loginRequired: true, -}, { - path: '/admin/user/:userId', - component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')), -}, { - path: '/admin/file/:fileId', - component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), -}, { - path: '/admin', - component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')), - children: [{ - path: '/overview', - name: 'overview', - component: page(() => import('./pages/admin/overview.vue')), - }, { - path: '/users', - name: 'users', - component: page(() => import('./pages/admin/users.vue')), - }, { - path: '/emojis', - name: 'emojis', - component: page(() => import('./pages/custom-emojis-manager.vue')), - }, { - path: '/avatar-decorations', - name: 'avatarDecorations', - component: page(() => import('./pages/avatar-decorations.vue')), - }, { - path: '/queue', - name: 'queue', - component: page(() => import('./pages/admin/queue.vue')), - }, { - path: '/files', - name: 'files', - component: page(() => import('./pages/admin/files.vue')), - }, { - path: '/federation', - name: 'federation', - component: page(() => import('./pages/admin/federation.vue')), - }, { - path: '/announcements', - name: 'announcements', - component: page(() => import('./pages/admin/announcements.vue')), - }, { - path: '/ads', - name: 'ads', - component: page(() => import('./pages/admin/ads.vue')), - }, { - path: '/roles/:id/edit', - name: 'roles', - component: page(() => import('./pages/admin/roles.edit.vue')), - }, { - path: '/roles/new', - name: 'roles', - component: page(() => import('./pages/admin/roles.edit.vue')), - }, { - path: '/roles/:id', - name: 'roles', - component: page(() => import('./pages/admin/roles.role.vue')), - }, { - path: '/roles', - name: 'roles', - component: page(() => import('./pages/admin/roles.vue')), - }, { - path: '/database', - name: 'database', - component: page(() => import('./pages/admin/database.vue')), - }, { - path: '/abuses', - name: 'abuses', - component: page(() => import('./pages/admin/abuses.vue')), - }, { - path: '/modlog', - name: 'modlog', - component: page(() => import('./pages/admin/modlog.vue')), - }, { - path: '/settings', - name: 'settings', - component: page(() => import('./pages/admin/settings.vue')), - }, { - path: '/branding', - name: 'branding', - component: page(() => import('./pages/admin/branding.vue')), - }, { - path: '/moderation', - name: 'moderation', - component: page(() => import('./pages/admin/moderation.vue')), - }, { - path: '/email-settings', - name: 'email-settings', - component: page(() => import('./pages/admin/email-settings.vue')), - }, { - path: '/object-storage', - name: 'object-storage', - component: page(() => import('./pages/admin/object-storage.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('./pages/admin/security.vue')), - }, { - path: '/relays', - name: 'relays', - component: page(() => import('./pages/admin/relays.vue')), - }, { - path: '/instance-block', - name: 'instance-block', - component: page(() => import('./pages/admin/instance-block.vue')), - }, { - path: '/proxy-account', - name: 'proxy-account', - component: page(() => import('./pages/admin/proxy-account.vue')), - }, { - path: '/external-services', - name: 'external-services', - component: page(() => import('./pages/admin/external-services.vue')), - }, { - path: '/other-settings', - name: 'other-settings', - component: page(() => import('./pages/admin/other-settings.vue')), - }, { - path: '/server-rules', - name: 'server-rules', - component: page(() => import('./pages/admin/server-rules.vue')), - }, { - path: '/invites', - name: 'invites', - component: page(() => import('./pages/admin/invites.vue')), - }, { - path: '/approvals', - name: 'approvals', - component: page(() => import('./pages/admin/approvals.vue')), - }, { - path: '/', - component: page(() => import('./pages/_empty_.vue')), - }], -}, { - path: '/my/notifications', - component: page(() => import('./pages/notifications.vue')), - loginRequired: true, -}, { - path: '/my/favorites', - component: page(() => import('./pages/favorites.vue')), - loginRequired: true, -}, { - path: '/my/achievements', - component: page(() => import('./pages/achievements.vue')), - loginRequired: true, -}, { - path: '/my/drive/folder/:folder', - component: page(() => import('./pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive', - component: page(() => import('./pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive/file/:fileId', - component: page(() => import('./pages/drive.file.vue')), - loginRequired: true, -}, { - path: '/my/follow-requests', - component: page(() => import('./pages/follow-requests.vue')), - loginRequired: true, -}, { - path: '/my/lists/:listId', - component: page(() => import('./pages/my-lists/list.vue')), - loginRequired: true, -}, { - path: '/my/lists', - component: page(() => import('./pages/my-lists/index.vue')), - loginRequired: true, -}, { - path: '/my/clips', - component: page(() => import('./pages/my-clips/index.vue')), - loginRequired: true, -}, { - path: '/my/antennas/create', - component: page(() => import('./pages/my-antennas/create.vue')), - loginRequired: true, -}, { - path: '/my/antennas/:antennaId', - component: page(() => import('./pages/my-antennas/edit.vue')), - loginRequired: true, -}, { - path: '/my/antennas', - component: page(() => import('./pages/my-antennas/index.vue')), - loginRequired: true, -}, { - path: '/timeline/list/:listId', - component: page(() => import('./pages/user-list-timeline.vue')), - loginRequired: true, -}, { - path: '/timeline/antenna/:antennaId', - component: page(() => import('./pages/antenna-timeline.vue')), - loginRequired: true, -}, { - path: '/clicker', - component: page(() => import('./pages/clicker.vue')), - loginRequired: true, -}, { - path: '/timeline', - component: page(() => import('./pages/timeline.vue')), -}, { - name: 'index', - path: '/', - component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')), - globalCacheKey: 'index', -}, { - path: '/:(*)', - component: page(() => import('./pages/not-found.vue')), -}]; - -export const mainRouter = new Router(routes, location.pathname + location.search + location.hash, !!$i, page(() => import('@/pages/not-found.vue'))); - -window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); - -mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); -}); - -window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); -}); - -export function useRouter(): Router { - return inject('router', null) ?? mainRouter; -} diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index e7585fcf8..67d997f09 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; export const ACHIEVEMENT_TYPES = [ @@ -83,6 +83,8 @@ export const ACHIEVEMENT_TYPES = [ 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', + 'bubbleGameExplodingHead', + 'bubbleGameDoubleExplodingHead', ] as const; export const ACHIEVEMENT_BADGES = { @@ -466,6 +468,16 @@ export const ACHIEVEMENT_BADGES = { bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', frame: 'bronze', }, + 'bubbleGameExplodingHead': { + img: '/fluent-emoji/1f92f.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'bronze', + }, + 'bubbleGameDoubleExplodingHead': { + img: '/fluent-emoji/1f92f.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'silver', + }, /* @see } as const satisfies Record { claimingQueue.delete(type); }, 500); - os.api('i/claim-achievement', { name: type }); + misskeyApi('i/claim-achievement', { name: type }); } if (_DEV_) { diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 038ae2310..c13849cc8 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -5,12 +5,23 @@ import { utils, values } from '@syuilo/aiscript'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; import { url, lang } from '@/config.js'; import { nyaize } from '@/scripts/nyaize.js'; +export function aiScriptReadline(q: string): Promise { + return new Promise(ok => { + os.inputText({ + title: q, + }).then(({ result: a }) => { + ok(a ?? ''); + }); + }); +} + export function createAiScriptEnv(opts) { return { USER_ID: $i ? values.STR($i.id) : values.NULL, @@ -44,7 +55,7 @@ export function createAiScriptEnv(opts) { if (typeof token.value !== 'string') throw new Error('invalid token'); } const actualToken: string|null = token?.value ?? opts.token ?? null; - return os.api(ep.value, utils.valToJs(param), actualToken).then(res => { + return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => { return utils.jsToVal(res); }, err => { return values.ERROR('request_failed', utils.jsToVal(err)); diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 08ba1e6d9..215ac4cc6 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -218,7 +218,7 @@ function getTextOptions(def: values.Value | undefined): Omit { +function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Omit { utils.assertObject(def); const text = def.value.get('text'); @@ -241,7 +241,7 @@ function getMfmOptions(def: values.Value | undefined): Omit { - if (onClickEv) call(onClickEv, values.STR(evId)); + if (onClickEv) call(onClickEv, [values.STR(evId)]); }, }; } diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts index 5ad076e5e..360bea903 100644 --- a/packages/frontend/src/scripts/clicker-game.ts +++ b/packages/frontend/src/scripts/clicker-game.ts @@ -4,7 +4,7 @@ */ import { ref, computed } from 'vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; type SaveData = { gameVersion: number; @@ -23,7 +23,7 @@ let prev = ''; export async function load() { try { - saveData.value = await os.api('i/registry/get', { + saveData.value = await misskeyApi('i/registry/get', { scope: ['clickerGame'], key: 'saveData', }); @@ -63,7 +63,7 @@ export async function save() { const current = JSON.stringify(saveData.value); if (current === prev) return; - await os.api('i/registry/set', { + await misskeyApi('i/registry/set', { scope: ['clickerGame'], key: 'saveData', value: saveData.value, diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts new file mode 100644 index 000000000..41af9cb7a --- /dev/null +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -0,0 +1,468 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Matter from 'matter-js'; +import seedrandom from 'seedrandom'; + +export type Mono = { + id: string; + level: number; + size: number; + shape: 'circle' | 'rectangle'; + score: number; + dropCandidate: boolean; + sfxPitch: number; + img: string; + imgSize: number; + spriteScale: number; +}; + +type Log = { + frame: number; + operation: 'drop'; + x: number; +} | { + frame: number; + operation: 'hold'; +} | { + frame: number; + operation: 'surrender'; +}; + +export class DropAndFusionGame extends EventEmitter<{ + changeScore: (newScore: number) => void; + changeCombo: (newCombo: number) => void; + changeStock: (newStock: { id: string; mono: Mono }[]) => void; + changeHolding: (newHolding: { id: string; mono: Mono } | null) => void; + dropped: (x: number) => void; + fusioned: (x: number, y: number, scoreDelta: number) => void; + monoAdded: (mono: Mono) => void; + gameOver: () => void; + sfx(type: string, params: { volume: number; pan: number; pitch: number; }): void; +}> { + private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる + private COMBO_INTERVAL = 60; // frame + public readonly GAME_VERSION = 1; + public readonly GAME_WIDTH = 450; + public readonly GAME_HEIGHT = 600; + public readonly DROP_INTERVAL = 500; + public readonly PLAYAREA_MARGIN = 25; + private STOCK_MAX = 4; + private TICK_DELTA = 1000 / 60; // 60fps + + public frame = 0; + public engine: Matter.Engine; + private tickCallbackQueue: { frame: number; callback: () => void; }[] = []; + private overflowCollider: Matter.Body; + private isGameOver = false; + private monoDefinitions: Mono[] = []; + private rng: () => number; + private logs: Log[] = []; + private replaying = false; + + /** + * フィールドに出ていて、かつ合体の対象となるアイテム + */ + private activeBodyIds: Matter.Body['id'][] = []; + + /** + * fusion予約アイテムのペア + * TODO: これらのモノは光らせるなどの演出をすると視覚的に楽しそう + */ + private fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = []; + + private latestDroppedBodyId: Matter.Body['id'] | null = null; + + private latestDroppedAt = 0; + private latestFusionedAt = 0; // frame + private stock: { id: string; mono: Mono }[] = []; + private holding: { id: string; mono: Mono } | null = null; + + private _combo = 0; + private get combo() { + return this._combo; + } + private set combo(value: number) { + this._combo = value; + this.emit('changeCombo', value); + } + + private _score = 0; + private get score() { + return this._score; + } + private set score(value: number) { + this._score = value; + this.emit('changeScore', value); + } + + public replayPlaybackRate = 1; + + constructor(env: { monoDefinitions: Mono[]; seed: string; replaying?: boolean }) { + super(); + + this.replaying = !!env.replaying; + this.monoDefinitions = env.monoDefinitions; + this.rng = seedrandom(env.seed); + + this.tick = this.tick.bind(this); + + this.engine = Matter.Engine.create({ + constraintIterations: 2 * this.PHYSICS_QUALITY_FACTOR, + positionIterations: 6 * this.PHYSICS_QUALITY_FACTOR, + velocityIterations: 4 * this.PHYSICS_QUALITY_FACTOR, + gravity: { + x: 0, + y: 1, + }, + timing: { + timeScale: 2, + }, + enableSleeping: false, + }); + + this.engine.world.bodies = []; + + //#region walls + const WALL_OPTIONS: Matter.IChamferableBodyDefinition = { + label: '_wall_', + isStatic: true, + friction: 0.7, + slop: 1.0, + render: { + strokeStyle: 'transparent', + fillStyle: 'transparent', + }, + }; + + const thickness = 100; + Matter.Composite.add(this.engine.world, [ + Matter.Bodies.rectangle(this.GAME_WIDTH / 2, this.GAME_HEIGHT + (thickness / 2) - this.PLAYAREA_MARGIN, this.GAME_WIDTH, thickness, WALL_OPTIONS), + Matter.Bodies.rectangle(this.GAME_WIDTH + (thickness / 2) - this.PLAYAREA_MARGIN, this.GAME_HEIGHT / 2, thickness, this.GAME_HEIGHT, WALL_OPTIONS), + Matter.Bodies.rectangle(-((thickness / 2) - this.PLAYAREA_MARGIN), this.GAME_HEIGHT / 2, thickness, this.GAME_HEIGHT, WALL_OPTIONS), + ]); + //#endregion + + this.overflowCollider = Matter.Bodies.rectangle(this.GAME_WIDTH / 2, 0, this.GAME_WIDTH, 200, { + isStatic: true, + isSensor: true, + render: { + strokeStyle: 'transparent', + fillStyle: 'transparent', + }, + }); + Matter.Composite.add(this.engine.world, this.overflowCollider); + } + + private msToFrame(ms: number) { + return Math.round(ms / this.TICK_DELTA); + } + + private createBody(mono: Mono, x: number, y: number) { + const options: Matter.IBodyDefinition = { + label: mono.id, + //density: 0.0005, + density: mono.size / 1000, + restitution: 0.2, + frictionAir: 0.01, + friction: 0.7, + frictionStatic: 5, + slop: 1.0, + //mass: 0, + render: { + sprite: { + texture: mono.img, + xScale: (mono.size / mono.imgSize) * mono.spriteScale, + yScale: (mono.size / mono.imgSize) * mono.spriteScale, + }, + }, + }; + if (mono.shape === 'circle') { + return Matter.Bodies.circle(x, y, mono.size / 2, options); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if (mono.shape === 'rectangle') { + return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options); + } else { + throw new Error('unrecognized shape'); + } + } + + private fusion(bodyA: Matter.Body, bodyB: Matter.Body) { + if (this.latestFusionedAt > this.frame - this.COMBO_INTERVAL) { + this.combo++; + } else { + this.combo = 1; + } + this.latestFusionedAt = this.frame; + + // TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する? + const newX = (bodyA.position.x + bodyB.position.x) / 2; + const newY = (bodyA.position.y + bodyB.position.y) / 2; + + Matter.Composite.remove(this.engine.world, [bodyA, bodyB]); + this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id); + + const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!; + const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1); + + if (nextMono) { + const body = this.createBody(nextMono, newX, newY); + Matter.Composite.add(this.engine.world, body); + + // 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする + this.tickCallbackQueue.push({ + frame: this.frame + this.msToFrame(100), + callback: () => { + this.activeBodyIds.push(body.id); + }, + }); + + const comboBonus = 1 + ((this.combo - 1) / 5); + const additionalScore = Math.round(currentMono.score * comboBonus); + this.score += additionalScore; + + this.emit('monoAdded', nextMono); + this.emit('fusioned', newX, newY, additionalScore); + + const panV = newX - this.PLAYAREA_MARGIN; + const panW = this.GAME_WIDTH - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN; + const pan = ((panV / panW) - 0.5) * 2; + this.emit('sfx', 'fusion', { volume: 1, pan, pitch: nextMono.sfxPitch }); + } else { + // nop + } + } + + private onCollision(event: Matter.IEventCollision) { + const minCollisionEnergyForSound = 2.5; + const maxCollisionEnergyForSound = 9; + const soundPitchMax = 4; + const soundPitchMin = 0.5; + + for (const pairs of event.pairs) { + const { bodyA, bodyB } = pairs; + + if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) { + if (bodyA.id === this.latestDroppedBodyId || bodyB.id === this.latestDroppedBodyId) { + continue; + } + this.gameOver(); + break; + } + + const shouldFusion = (bodyA.label === bodyB.label) && + !this.fusionReservedPairs.some(x => + x.bodyA.id === bodyA.id || + x.bodyA.id === bodyB.id || + x.bodyB.id === bodyA.id || + x.bodyB.id === bodyB.id); + + if (shouldFusion) { + if (this.activeBodyIds.includes(bodyA.id) && this.activeBodyIds.includes(bodyB.id)) { + this.fusion(bodyA, bodyB); + } else { + this.fusionReservedPairs.push({ bodyA, bodyB }); + this.tickCallbackQueue.push({ + frame: this.frame + this.msToFrame(100), + callback: () => { + this.fusionReservedPairs = this.fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); + this.fusion(bodyA, bodyB); + }, + }); + } + } else { + const energy = pairs.collision.depth; + if (energy > minCollisionEnergyForSound) { + const volume = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4; + const panV = + pairs.bodyA.label === '_wall_' ? bodyB.position.x - this.PLAYAREA_MARGIN : + pairs.bodyB.label === '_wall_' ? bodyA.position.x - this.PLAYAREA_MARGIN : + ((bodyA.position.x + bodyB.position.x) / 2) - this.PLAYAREA_MARGIN; + const panW = this.GAME_WIDTH - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN; + const pan = ((panV / panW) - 0.5) * 2; + const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10))); + this.emit('sfx', 'collision', { volume, pan, pitch }); + } + } + } + } + + public surrender() { + this.logs.push({ + frame: this.frame, + operation: 'surrender', + }); + + this.gameOver(); + } + + private gameOver() { + this.isGameOver = true; + this.emit('gameOver'); + } + + public start() { + for (let i = 0; i < this.STOCK_MAX; i++) { + this.stock.push({ + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + }); + } + this.emit('changeStock', this.stock); + + Matter.Events.on(this.engine, 'collisionStart', this.onCollision.bind(this)); + } + + public getLogs() { + return this.logs; + } + + public tick() { + this.frame++; + + if (this.latestFusionedAt < this.frame - this.COMBO_INTERVAL) { + this.combo = 0; + } + + this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { + if (x.frame === this.frame) { + x.callback(); + return false; + } else { + return true; + } + }); + + Matter.Engine.update(this.engine, this.TICK_DELTA); + + const hasNextTick = !this.isGameOver; + + return hasNextTick; + } + + public getActiveMonos() { + return this.engine.world.bodies.map(x => this.monoDefinitions.find((mono) => mono.id === x.label)!).filter(x => x !== undefined); + } + + public drop(_x: number) { + if (this.isGameOver) return; + // TODO: フレームで計算するようにすればリプレイかどうかのチェックは不要になる + if (!this.replaying && (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL)) return; + + const head = this.stock.shift()!; + this.stock.push({ + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + }); + this.emit('changeStock', this.stock); + + const inputX = Math.round(_x); + const x = Math.min(this.GAME_WIDTH - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), inputX)); + const body = this.createBody(head.mono, x, 50 + head.mono.size / 2); + this.logs.push({ + frame: this.frame, + operation: 'drop', + x: inputX, + }); + Matter.Composite.add(this.engine.world, body); + this.activeBodyIds.push(body.id); + this.latestDroppedBodyId = body.id; + this.latestDroppedAt = Date.now(); + this.emit('dropped', x); + this.emit('monoAdded', head.mono); + } + + public hold() { + if (this.isGameOver) return; + + this.logs.push({ + frame: this.frame, + operation: 'hold', + }); + + if (this.holding) { + const head = this.stock.shift()!; + this.stock.unshift(this.holding); + this.holding = head; + this.emit('changeHolding', this.holding); + this.emit('changeStock', this.stock); + } else { + const head = this.stock.shift()!; + this.holding = head; + this.stock.push({ + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + }); + this.emit('changeHolding', this.holding); + this.emit('changeStock', this.stock); + } + } + + public static serializeLogs(logs: Log[]) { + const _logs: number[][] = []; + + for (let i = 0; i < logs.length; i++) { + const log = logs[i]; + const frameDelta = i === 0 ? log.frame : log.frame - logs[i - 1].frame; + + switch (log.operation) { + case 'drop': + _logs.push([frameDelta, 0, log.x]); + break; + case 'hold': + _logs.push([frameDelta, 1]); + break; + case 'surrender': + _logs.push([frameDelta, 2]); + break; + } + } + + return _logs; + } + + public static deserializeLogs(logs: number[][]) { + const _logs: Log[] = []; + + let frame = 0; + + for (const log of logs) { + const frameDelta = log[0]; + frame += frameDelta; + + const operation = log[1]; + + switch (operation) { + case 0: + _logs.push({ + frame, + operation: 'drop', + x: log[2], + }); + break; + case 1: + _logs.push({ + frame, + operation: 'hold', + }); + break; + case 2: + _logs.push({ + frame, + operation: 'surrender', + }); + break; + } + } + + return _logs; + } + + public dispose() { + Matter.World.clear(this.engine.world, false); + Matter.Engine.clear(this.engine); + } +} diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts index 46a13462a..62a495c2d 100644 --- a/packages/frontend/src/scripts/emoji-base.ts +++ b/packages/frontend/src/scripts/emoji-base.ts @@ -5,6 +5,7 @@ const twemojiSvgBase = '/twemoji'; const fluentEmojiPngBase = '/fluent-emoji'; +const tossfaceSvgBase = '/tossface'; export function char2twemojiFilePath(char: string): string { let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); @@ -23,3 +24,14 @@ export function char2fluentEmojiFilePath(char: string): string { const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); return `${fluentEmojiPngBase}/${fileName}.png`; } + +export function char2tossfaceFilePath(char: string): string { + let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); + // Twemoji is the only emoji font which still supports the shibuya 50 emoji to this day + if (codes[0]?.startsWith('e50a')) return char2twemojiFilePath(char); + // Tossface does not use the fe0f modifier + codes = codes.filter(x => x !== 'fe0f'); + codes = codes.filter(x => x && x.length); + const fileName = codes.join('-'); + return `${tossfaceSvgBase}/${fileName}.svg`; +} diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts index 8885bf4b7..4bd8bf94b 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend/src/scripts/emojilist.ts @@ -36,7 +36,8 @@ for (let i = 0; i < emojilist.length; i++) { export const emojiCharByCategory = _charGroupByCategory; export function getEmojiName(char: string): string | null { - const idx = _indexByChar.get(char); + // Colorize it because emojilist.json assumes that + const idx = _indexByChar.get(colorizeEmoji(char)); if (idx == null) { return null; } else { @@ -44,6 +45,10 @@ export function getEmojiName(char: string): string | null { } } +export function colorizeEmoji(char: string) { + return char.length === 1 ? `${char}\uFE0F` : char; +} + export interface CustomEmojiFolderTree { value: string; category: string; diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts index 222fd9b0b..f7e036941 100644 --- a/packages/frontend/src/scripts/form.ts +++ b/packages/frontend/src/scripts/form.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -type EnumItem = string | {label: string; value: string;}; +type EnumItem = string | { + label: string; + value: string; +}; + export type FormItem = { label?: string; type: 'string'; @@ -36,16 +40,23 @@ export type FormItem = { label: string; value: unknown; }[]; +} | { + label?: string; + type: 'range'; + default: number | null; + step: number; + min: number; + max: number; } | { label?: string; type: 'object'; default: Record | null; - hidden: true; + hidden: boolean; } | { label?: string; type: 'array'; default: unknown[] | null; - hidden: true; + hidden: boolean; }; export type Form = Record; @@ -55,6 +66,7 @@ type GetItemType = Item['type'] extends 'number' ? number : Item['type'] extends 'boolean' ? boolean : Item['type'] extends 'radio' ? unknown : + Item['type'] extends 'range' ? number : Item['type'] extends 'enum' ? string : Item['type'] extends 'array' ? unknown[] : Item['type'] extends 'object' ? Record diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts index 54654980f..068cd9cd9 100644 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -18,7 +18,7 @@ export async function genSearchQuery(v: any, q: string) { host = at; } } else { - const user = await v.os.api('users/show', Misskey.acct.parse(at)).catch(x => null); + const user = await v.api('users/show', Misskey.acct.parse(at)).catch(x => null); if (user) { userId = user.id; } else { diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index d6a5b00c0..b30f87c91 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -8,6 +8,7 @@ import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; @@ -18,7 +19,7 @@ function rename(file: Misskey.entities.DriveFile) { default: file.name, }).then(({ canceled, result: name }) => { if (canceled) return; - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, name: name, }); @@ -31,7 +32,7 @@ function describe(file: Misskey.entities.DriveFile) { file: file, }, { done: caption => { - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, comment: caption.length === 0 ? null : caption, }); @@ -40,7 +41,7 @@ function describe(file: Misskey.entities.DriveFile) { } function toggleSensitive(file: Misskey.entities.DriveFile) { - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive, }).catch(err => { @@ -69,7 +70,7 @@ async function deleteFile(file: Misskey.entities.DriveFile) { }); if (canceled) return; - os.api('drive/files/delete', { + misskeyApi('drive/files/delete', { fileId: file.id, }); } diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index a409f1b77..f6db7c48a 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -10,6 +10,7 @@ import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; import { defaultStore, noteActions } from '@/store.js'; @@ -40,7 +41,7 @@ export async function getNoteClipMenu(props: { action: () => { claimAchievement('noteClipped1'); os.promiseDialog( - os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), + misskeyApi('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), null, async (err) => { if (err.id === '734806c4-542c-463a-9311-15c512803965') { @@ -156,7 +157,7 @@ export function getNoteMenu(props: { }).then(({ canceled }) => { if (canceled) return; - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: appearNote.id, }); @@ -173,7 +174,7 @@ export function getNoteMenu(props: { }).then(({ canceled }) => { if (canceled) return; - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: appearNote.id, }); @@ -265,7 +266,7 @@ export function getNoteMenu(props: { async function translate(): Promise { if (props.translation.value != null) return; props.translating.value = true; - const res = await os.api('notes/translate', { + const res = await misskeyApi('notes/translate', { noteId: appearNote.id, targetLang: miLocalStorage.getItem('lang') ?? navigator.language, }); @@ -275,7 +276,7 @@ export function getNoteMenu(props: { let menu: MenuItem[]; if ($i) { - const statePromise = os.api('notes/state', { + const statePromise = misskeyApi('notes/state', { noteId: appearNote.id, }); @@ -355,7 +356,7 @@ export function getNoteMenu(props: { icon: 'ph-user ph-bold ph-lg', text: i18n.ts.user, children: async () => { - const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId }); + const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); const { menu, cleanup } = getUserMenu(user); cleanups.push(cleanup); return menu; @@ -377,6 +378,42 @@ export function getNoteMenu(props: { ] : [] ), + ...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [ + { type: 'divider' }, + { + type: 'parent' as const, + icon: 'ti ti-device-tv', + text: i18n.ts.channel, + children: async () => { + const channelChildMenu = [] as MenuItem[]; + + const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); + + if (channel.pinnedNoteIds.includes(appearNote.id)) { + channelChildMenu.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), + }), + }); + } else { + channelChildMenu.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], + }), + }); + } + return channelChildMenu; + }, + }, + ] + : [] + ), ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ { type: 'divider' }, appearNote.userId === $i.id ? { @@ -497,7 +534,7 @@ export function getRenoteMenu(props: { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: appearNote.id, channelId: appearNote.channelId, }).then(() => { @@ -542,7 +579,7 @@ export function getRenoteMenu(props: { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { localOnly, visibility, renoteId: appearNote.id, diff --git a/packages/frontend/src/scripts/get-note-versions-menu.ts b/packages/frontend/src/scripts/get-note-versions-menu.ts index 46e3bab3a..e05e2e91b 100644 --- a/packages/frontend/src/scripts/get-note-versions-menu.ts +++ b/packages/frontend/src/scripts/get-note-versions-menu.ts @@ -2,6 +2,7 @@ import { Ref, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from './misskey-api.js'; import { MenuItem } from '@/types/menu.js'; import { dateTimeFormat } from './intl-const.js'; @@ -30,7 +31,7 @@ export async function getNoteVersionsMenu(props: { } const menu: MenuItem[] = []; - const statePromise = os.api('notes/versions', { + const statePromise = misskeyApi('notes/versions', { noteId: appearNote.id, }); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 67bc781ae..35eeded7e 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -10,13 +10,14 @@ import { i18n } from '@/i18n.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { host, url } from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, userActions } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; -import { mainRouter } from '@/router.js'; -import { Router } from '@/nirax.js'; +import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; +import { mainRouter } from '@/global/router/main.js'; -export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) { +export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; const cleanups = [] as (() => void)[]; @@ -131,7 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router } async function editMemo(): Promise { - const userDetailed = await os.api('users/show', { + const userDetailed = await misskeyApi('users/show', { userId: user.id, }); const { canceled, result } = await os.form(i18n.ts.editMemo, { diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts index 1310a0dc7..ee218df01 100644 --- a/packages/frontend/src/scripts/install-plugin.ts +++ b/packages/frontend/src/scripts/install-plugin.ts @@ -10,6 +10,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import type { Plugin } from '@/store.js'; import { ColdDeviceStorage } from '@/store.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; export type AiScriptPluginMeta = { @@ -110,7 +111,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { }, { done: async result => { const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { + const { token } = await misskeyApi('miauth/gen-token', { session: null, name: name, permission: permissions, diff --git a/packages/frontend/src/scripts/lookup-user.ts b/packages/frontend/src/scripts/lookup-user.ts index a35fe898e..9ae5eccb7 100644 --- a/packages/frontend/src/scripts/lookup-user.ts +++ b/packages/frontend/src/scripts/lookup-user.ts @@ -6,6 +6,7 @@ import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; export async function lookupUser() { const { canceled, result } = await os.inputText({ @@ -17,8 +18,8 @@ export async function lookupUser() { os.pageWindow(`/admin/user/${user.id}`); }; - const usernamePromise = os.api('users/show', Misskey.acct.parse(result)); - const idPromise = os.api('users/show', { userId: result }); + const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result)); + const idPromise = misskeyApi('users/show', { userId: result }); let _notFound = false; const notFound = () => { if (_notFound) { diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts index 979f40f03..ddcfd8852 100644 --- a/packages/frontend/src/scripts/lookup.ts +++ b/packages/frontend/src/scripts/lookup.ts @@ -4,9 +4,10 @@ */ import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { Router } from '@/nirax.js'; +import { mainRouter } from '@/global/router/main.js'; export async function lookup(router?: Router) { const _router = router ?? mainRouter; @@ -28,7 +29,7 @@ export async function lookup(router?: Router) { } if (query.startsWith('https://')) { - const promise = os.api('ap/show', { + const promise = misskeyApi('ap/show', { uri: query, }); diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 465926fe0..6e25cc856 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -25,7 +25,7 @@ function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textR MFM_TAGS.forEach(tag => { ret.push({ text: tag, - icon: 'ti ti-icons', + icon: 'ph-brackets-curly ph-bold ph-lg', action: () => add(textArea, textRef, tag), }); }); diff --git a/packages/frontend/src/scripts/api.ts b/packages/frontend/src/scripts/misskey-api.ts similarity index 71% rename from packages/frontend/src/scripts/api.ts rename to packages/frontend/src/scripts/misskey-api.ts index 8f3a16393..337fa1511 100644 --- a/packages/frontend/src/scripts/api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -10,12 +10,17 @@ import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); // Implements Misskey.api.ApiClient.request -export function api( +export function misskeyApi< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT, +>( endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal, -): Promise> { +): Promise<_ResT> { if (endpoint.includes('://')) throw new Error('invalid endpoint'); pendingApiRequestsCount.value++; @@ -23,7 +28,7 @@ export function api((resolve, reject) => { + const promise = new Promise<_ResT>((resolve, reject) => { // Append a credential if ($i) (data as any).i = $i.token; if (token !== undefined) (data as any).i = token; @@ -44,7 +49,7 @@ export function api undefined } else { reject(body.error); } @@ -57,10 +62,15 @@ export function api( +export function misskeyApiGet< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT, +>( endpoint: E, data: P = {} as any, -): Promise> { +): Promise<_ResT> { pendingApiRequestsCount.value++; const onFinally = () => { @@ -69,7 +79,7 @@ export function apiGet((resolve, reject) => { + const promise = new Promise<_ResT>((resolve, reject) => { // Send request window.fetch(`${apiUrl}/${endpoint}?${query}`, { method: 'GET', @@ -81,7 +91,7 @@ export function apiGet undefined } else { reject(body.error); } diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index 674c762fa..7a2d5a9b9 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -6,6 +6,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -65,7 +66,7 @@ export function chooseFileFromUrl(): Promise { } }); - os.api('drive/files/upload-from-url', { + misskeyApi('drive/files/upload-from-url', { url: url, folderId: defaultStore.state.uploadFolder, marker, diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 2f7545ef0..9c74a8b87 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -5,7 +5,6 @@ import type { SoundStore } from '@/store.js'; import { defaultStore } from '@/store.js'; -import * as os from '@/os.js'; let ctx: AudioContext; const cache = new Map(); @@ -89,63 +88,33 @@ export type OperationType = typeof operationTypes[number]; /** * 音声を読み込む - * @param soundStore サウンド設定 + * @param url url * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする */ -export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) { - if (_DEV_) console.log('loading audio. opts:', options); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { - return; - } +export async function loadAudio(url: string, options?: { useCache?: boolean; }) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ctx == null) { ctx = new AudioContext(); } if (options?.useCache ?? true) { - if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) { - if (_DEV_) console.log('use cache'); - return cache.get(soundStore.fileId) as AudioBuffer; - } else if (cache.has(soundStore.type)) { - if (_DEV_) console.log('use cache'); - return cache.get(soundStore.type) as AudioBuffer; + if (cache.has(url)) { + return cache.get(url) as AudioBuffer; } } let response: Response; - if (soundStore.type === '_driveFile_') { - try { - response = await fetch(soundStore.fileUrl); - } catch (err) { - try { - // URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック - const apiRes = await os.api('drive/files/show', { - fileId: soundStore.fileId, - }); - response = await fetch(apiRes.url); - } catch (fbErr) { - // それでも無理なら諦める - return; - } - } - } else { - try { - response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`); - } catch (err) { - return; - } + try { + response = await fetch(url); + } catch (err) { + return; } const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); if (options?.useCache ?? true) { - if (soundStore.type === '_driveFile_') { - cache.set(soundStore.fileId, audioBuffer); - } else { - cache.set(soundStore.type, audioBuffer); - } + cache.set(url, audioBuffer); } return audioBuffer; @@ -155,13 +124,12 @@ export async function loadAudio(soundStore: SoundStore, options?: { useCache?: b * 既定のスプライトを再生する * @param type スプライトの種類を指定 */ -export function play(operationType: OperationType) { +export function playMisskeySfx(operationType: OperationType) { const sound = defaultStore.state[`sound_${operationType}`]; - if (_DEV_) console.log('play', operationType, sound); if (sound.type == null || !canPlay) return; canPlay = false; - playFile(sound).finally(() => { + playMisskeySfxFile(sound).finally(() => { // ごく短時間に音が重複しないように setTimeout(() => { canPlay = true; @@ -173,26 +141,59 @@ export function play(operationType: OperationType) { * サウンド設定形式で指定された音声を再生する * @param soundStore サウンド設定 */ -export async function playFile(soundStore: SoundStore) { - const buffer = await loadAudio(soundStore); +export async function playMisskeySfxFile(soundStore: SoundStore) { + if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { + return; + } + const masterVolume = defaultStore.state.sound_masterVolume; + if (isMute() || masterVolume === 0 || soundStore.volume === 0) { + return; + } + const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; + const buffer = await loadAudio(url); if (!buffer) return; - createSourceNode(buffer, soundStore.volume)?.start(); + const volume = soundStore.volume * masterVolume; + createSourceNode(buffer, { volume }).soundSource.start(); } -export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { - const masterVolume = defaultStore.state.sound_masterVolume; - if (isMute() || masterVolume === 0 || volume === 0) { - return null; +export async function playUrl(url: string, opts: { + volume?: number; + pan?: number; + playbackRate?: number; +}) { + if (opts.volume === 0) { + return; } + const buffer = await loadAudio(url); + if (!buffer) return; + createSourceNode(buffer, opts).soundSource.start(); +} + +export function createSourceNode(buffer: AudioBuffer, opts: { + volume?: number; + pan?: number; + playbackRate?: number; +}): { + soundSource: AudioBufferSourceNode; + panNode: StereoPannerNode; + gainNode: GainNode; +} { + const panNode = ctx.createStereoPanner(); + panNode.pan.value = opts.pan ?? 0; const gainNode = ctx.createGain(); - gainNode.gain.value = masterVolume * volume; + + gainNode.gain.value = opts.volume ?? 1; const soundSource = ctx.createBufferSource(); soundSource.buffer = buffer; - soundSource.connect(gainNode).connect(ctx.destination); + soundSource.playbackRate.value = opts.playbackRate ?? 1; + soundSource + .connect(panNode) + .connect(gainNode) + .connect(ctx.destination); - return soundSource; + return { soundSource, panNode, gainNode }; } /** diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 3bf6d5798..a174f5175 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -44,6 +44,8 @@ export const getBuiltinThemes = () => Promise.all( 'd-cherry', 'd-ice', 'd-u0', + 'rosepine', + 'rosepine-dawn', ].map(name => import(`@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), ); diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index b896376ec..14b667fd6 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -5,7 +5,7 @@ import { reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { readAndCompressImage } from 'browser-image-resizer'; +import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { getCompressionConfig } from './upload/compress-config.js'; import { defaultStore } from '@/store.js'; import { apiUrl } from '@/config.js'; diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts index 2deb9cbb8..4775213c2 100644 --- a/packages/frontend/src/scripts/upload/compress-config.ts +++ b/packages/frontend/src/scripts/upload/compress-config.ts @@ -5,7 +5,7 @@ import isAnimated from 'is-file-animated'; import { isWebpSupported } from './isWebpSupported.js'; -import type { BrowserImageResizerConfig } from 'browser-image-resizer'; +import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer'; const compressTypeMap = { 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' }, @@ -21,7 +21,7 @@ const compressTypeMapFallback = { 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, } as const; -export async function getCompressionConfig(file: File): Promise { +export async function getCompressionConfig(file: File): Promise { const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type]; if (!imgConfig || await isAnimated(file)) { return; diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index 427bc6ff3..fb31fce1d 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -8,6 +8,7 @@ import * as Misskey from 'misskey-js'; import { useStream } from '@/stream.js'; import { $i } from '@/account.js'; import * as os from '@/os.js'; +import { misskeyApi } from './misskey-api.js'; export function useNoteCapture(props: { rootEl: Ref; @@ -30,11 +31,15 @@ export function useNoteCapture(props: { case 'replied': { if (!props.onReplyCallback) break; - const replyNote = await os.api("notes/show", { - noteId: body.id, - }); + // notes/show may throw if the current user can't see the note + try { + const replyNote = await misskeyApi('notes/show', { + noteId: body.id, + }); - await props.onReplyCallback(replyNote); + await props.onReplyCallback(replyNote); + } catch { /* empty */ } + break; } @@ -95,17 +100,20 @@ export function useNoteCapture(props: { } case 'updated': { - const editedNote = await os.api("notes/show", { - noteId: id, - }); + try { + const editedNote = await misskeyApi('notes/show', { + noteId: id, + }); + + const keys = new Set(); + Object.keys(editedNote) + .concat(Object.keys(note.value)) + .forEach((key) => keys.add(key)); + keys.forEach((key) => { + note.value[key] = editedNote[key]; + }); + } catch { /* empty */ } - const keys = new Set(); - Object.keys(editedNote) - .concat(Object.keys(note.value)) - .forEach((key) => keys.add(key)); - keys.forEach((key) => { - note.value[key] = editedNote[key]; - }); break; } } diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 18cfad210..ea7c24800 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -151,6 +151,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: true, }, + showVisibilitySelectorOnBoost: { + where: 'account', + default: true, + }, + visibilityOnBoost: { + where: 'account', + default: 'public' as 'public' | 'home' | 'followers' | 'local', + }, menu: { where: 'deviceAccount', @@ -460,6 +468,13 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + dropAndFusion: { + where: 'device', + default: { + bgmVolume: 0.25, + sfxVolume: 1, + }, + }, sound_masterVolume: { where: 'device', diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index fd2716bf9..d8d0754ca 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -37,6 +37,9 @@ --margin: var(--marginHalf); } + --avatar: 48px; + --thread-width: 2px; + //--ad: rgb(255 169 0 / 10%); } diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts index f37c01cca..f96059b84 100644 --- a/packages/frontend/src/theme-store.ts +++ b/packages/frontend/src/theme-store.ts @@ -5,7 +5,7 @@ import { Theme, getBuiltinThemes } from '@/scripts/theme.js'; import { miLocalStorage } from '@/local-storage.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; const lsCacheKey = $i ? `themes:${$i.id}` as const : null; @@ -19,7 +19,7 @@ export async function fetchThemes(): Promise { if ($i == null) return; try { - const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' }); + const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } catch (err) { if (err.code === 'NO_SUCH_KEY') return; @@ -35,13 +35,13 @@ export async function addTheme(theme: Theme): Promise { } await fetchThemes(); const themes = getThemes().concat(theme); - await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); + await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } export async function removeTheme(theme: Theme): Promise { if ($i == null) return; const themes = getThemes().filter(t => t.id !== theme.id); - await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); + await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 index 3f5822977..854457271 100644 --- a/packages/frontend/src/themes/_dark.json5 +++ b/packages/frontend/src/themes/_dark.json5 @@ -30,6 +30,7 @@ panelHeaderFg: '@fg', panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelBorder: '" solid 1px var(--divider)', + thread: ':lighten<12<@panel', acrylicPanel: ':alpha<0.5<@panel', windowHeader: ':alpha<0.85<@panel', popup: ':lighten<3<@panel', diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 index 6ebfcaafe..2f3783310 100644 --- a/packages/frontend/src/themes/_light.json5 +++ b/packages/frontend/src/themes/_light.json5 @@ -30,6 +30,7 @@ panelHeaderFg: '@fg', panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelBorder: '" solid 1px var(--divider)', + thread: ':darken<12<@panel', acrylicPanel: ':alpha<0.5<@panel', windowHeader: ':alpha<0.85<@panel', popup: ':lighten<3<@panel', diff --git a/packages/frontend/src/themes/rosepine-dawn.json5 b/packages/frontend/src/themes/rosepine-dawn.json5 new file mode 100644 index 000000000..ff1ca0c99 --- /dev/null +++ b/packages/frontend/src/themes/rosepine-dawn.json5 @@ -0,0 +1,89 @@ +{ + id: '919c91ac-c6fa-43dc-a423-3cc84fd67d7c', + base: 'light', + name: ' Rosé Pine Dawn', + description: 'Soho vibes for Misskey, dawn edition', + props: { + accent: '#286983', + accentDarken: ':darken<10<@accent', + accentLighten: ':lighten<10<@accent', + accentedBg: ':alpha<0.15<@accent', + focus: ':alpha<0.3<@accent', + bg: '#faf4ed', + acrylicBg: ':alpha<0.5<@bg', + fg: '#575279', + fgTransparentWeak: ':alpha<0.75<@fg', + fgTransparent: ':alpha<0.5<@fg', + fgHighlighted: ':darken<3<@fg', + fgOnAccent: '#fffaf3', + divider: 'rgba(0, 0, 0, 0.1)', + indicator: '@accent', + panel: ':lighten<3<@bg', + panelHighlight: ':darken<3<@panel', + panelHeaderBg: ':lighten<3<@panel', + panelHeaderFg: '@fg', + panelHeaderDivider: 'rgba(0, 0, 0, 0)', + panelBorder: '" solid 1px var(--divider)', + acrylicPanel: ':alpha<0.5<@panel', + popup: ':lighten<3<@panel', + shadow: 'rgba(0, 0, 0, 0.1)', + header: ':alpha<0.7<@panel', + navBg: '@panel', + navFg: '@fg', + navHoverFg: ':darken<17<@fg', + navActive: '@accent', + navIndicator: '@indicator', + link: '#56949f', + hashtag: '#ea9d34', + mention: '@accent', + mentionMe: '@mention', + renote: '#56949f', + modalBg: 'rgba(0, 0, 0, 0.3)', + scrollbarHandle: 'rgba(0, 0, 0, 0.2)', + scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', + dateLabelFg: '@fg', + infoBg: '#f2e9e1', + infoFg: '#ea9d34', + infoWarnBg: '#f2e9e1', + infoWarnFg: '#b4637a', + switchBg: 'rgba(0, 0, 0, 0.15)', + cwBg: '#b4637a', + cwFg: '#faf4ed', + cwHoverBg: '#d7827e', + buttonBg: 'rgba(0, 0, 0, 0.05)', + buttonHoverBg: 'rgba(0, 0, 0, 0.1)', + buttonGradateA: '#d7827e', + buttonGradateB: ':hue<20<#d7827e', + inputBorder: 'rgba(0, 0, 0, 0.1)', + inputBorderHover: 'rgba(0, 0, 0, 0.2)', + listItemHoverBg: 'rgba(0, 0, 0, 0.03)', + driveFolderBg: ':alpha<0.3<@accent', + wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', + badge: '#31b1ce', + messageBg: '@bg', + success: '#907aa9', + error: '#b4637a', + warn: '#ea9d34', + codeString: '#b98710', + codeNumber: '#0fbbbb', + codeBoolean: '#62b70c', + htmlThemeColor: '@bg', + X2: ':darken<2<@panel', + X3: 'rgba(0, 0, 0, 0.05)', + X4: 'rgba(0, 0, 0, 0.1)', + X5: 'rgba(0, 0, 0, 0.05)', + X6: 'rgba(0, 0, 0, 0.25)', + X7: 'rgba(0, 0, 0, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', + X11: 'rgba(0, 0, 0, 0.1)', + X12: 'rgba(0, 0, 0, 0.1)', + X13: 'rgba(0, 0, 0, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', + }, + author: '@thatonecalculator@stop.voring.me', +} \ No newline at end of file diff --git a/packages/frontend/src/themes/rosepine.json5 b/packages/frontend/src/themes/rosepine.json5 new file mode 100644 index 000000000..06516f75f --- /dev/null +++ b/packages/frontend/src/themes/rosepine.json5 @@ -0,0 +1,86 @@ +{ + id: '3cdfd635-4d5e-4d06-9ba3-20f123f0999b', + base: 'dark', + desc: 'Soho vibes for Misskey', + name: 'Rosé Pine v3', + props: { + X2: ':darken<2<@panel', + X3: 'rgba(255, 255, 255, 0.05)', + X4: 'rgba(255, 255, 255, 0.1)', + X5: 'rgba(255, 255, 255, 0.05)', + X6: 'rgba(255, 255, 255, 0.15)', + X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + bg: '#191724', + fg: '#e0def4', + X10: ':alpha<0.4<@accent', + X11: 'rgba(0, 0, 0, 0.3)', + X12: 'rgba(255, 255, 255, 0.1)', + X13: 'rgba(255, 255, 255, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', + cwBg: '#1f1d2e', + cwFg: '#f6c177', + link: '#9ccfd8', + warn: '#f6c177', + badge: '#ebbcba', + error: '#eb6f92', + focus: ':alpha<0.3<@accent', + navBg: '@panel', + navFg: '@fg', + panel: ':lighten<3<@bg', + popup: ':lighten<3<@panel', + accent: '#c4a7e7', + header: ':alpha<0.7<@panel', + infoBg: '#253142', + infoFg: '#fff', + renote: '#31748f', + shadow: 'rgba(0, 0, 0, 0.3)', + divider: 'rgba(255, 255, 255, 0.1)', + hashtag: '#ebbcba', + mention: '@accent', + modalBg: 'rgba(0, 0, 0, 0.5)', + success: '#ebbcba', + buttonBg: 'rgba(255, 255, 255, 0.05)', + switchBg: 'rgba(255, 255, 255, 0.15)', + acrylicBg: ':alpha<0.5<@bg', + cwHoverBg: '#26233a', + indicator: '@accent', + mentionMe: '@mention', + messageBg: '@bg', + navActive: '@accent', + accentedBg: ':alpha<0.15<@accent', + fgOnAccent: '#26233a', + infoWarnBg: '#26233a', + infoWarnFg: '#f6c177', + navHoverFg: ':lighten<17<@fg', + dateLabelFg: '@fg', + inputBorder: 'rgba(255, 255, 255, 0.1)', + panelBorder: '" solid 1px var(--divider)', + accentDarken: ':darken<10<@accent', + acrylicPanel: ':alpha<0.5<@panel', + navIndicator: '@indicator', + accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', + driveFolderBg: ':alpha<0.3<@accent', + fgHighlighted: ':lighten<3<@fg', + fgTransparent: ':alpha<0.5<@fg', + panelHeaderBg: ':lighten<3<@panel', + panelHeaderFg: '@fg', + buttonGradateA: '@accent', + buttonGradateB: '#ebbcba', + htmlThemeColor: '@bg', + panelHighlight: ':lighten<3<@panel', + listItemHoverBg: 'rgba(255, 255, 255, 0.03)', + scrollbarHandle: 'rgba(255, 255, 255, 0.2)', + inputBorderHover: 'rgba(255, 255, 255, 0.2)', + wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', + fgTransparentWeak: ':alpha<0.75<@fg', + panelHeaderDivider: 'rgba(0, 0, 0, 0)', + scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', + }, + author: '@thatonecalculator@stop.voring.me', +} \ No newline at end of file diff --git a/packages/frontend/src/type.ts b/packages/frontend/src/type.ts new file mode 100644 index 000000000..9c0fc2a11 --- /dev/null +++ b/packages/frontend/src/type.ts @@ -0,0 +1,3 @@ +export type WithRequired = T & { [P in K]-?: T[P] }; + +export type WithNonNullable = T & { [P in K]-?: NonNullable }; diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index a3adbfb1b..6e2f8b7e4 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -27,6 +27,11 @@ function toolsMenuItems(): MenuItem[] { to: '/clicker', text: '🍪👈', icon: 'ph-cookie ph-bold ph-lg', + }, { + type: 'link', + to: '/bubble-game', + text: i18n.ts.bubbleGame, + icon: 'ph-orange-slice ph-bold ph-lg', }, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? { type: 'link', to: '/custom-emojis-manager', @@ -97,7 +102,13 @@ export function openInstanceMenu(ev: MouseEvent) { action: () => { window.open(instance.privacyPolicyUrl, '_blank', 'noopener'); }, - } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { + } : undefined, (instance.donationUrl) ? { + text: i18n.ts.donation, + icon: 'ph-hand-coins ph-bold ph-lg', + action: () => { + window.open(instance.donationUrl, '_blank', 'noopener'); + }, + } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) ? undefined : { type: 'divider' }, { text: i18n.ts.help, icon: 'ph-question ph-bold ph-lg', action: () => { diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 6ece7d86d..0ec036c5c 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -49,7 +49,8 @@ import { defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { swInject } from './sw-inject.js'; import XNotification from './notification.vue'; -import { popups, pendingApiRequestsCount } from '@/os.js'; +import { popups } from '@/os.js'; +import { pendingApiRequestsCount } from '@/scripts/misskey-api.js'; import { uploads } from '@/scripts/upload.js'; import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; @@ -82,7 +83,7 @@ function onNotification(notification: Misskey.entities.Notification, isClient = }, 6000); } - sound.play('notification'); + sound.playMisskeySfx('notification'); } if ($i) { diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index c92695afe..8df3b289d 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; @@ -52,7 +52,7 @@ const fetching = ref(true); const key = ref(0); const tick = () => { - os.api('federation/instances', { + misskeyApi('federation/instances', { sort: '+latestRequestReceivedAt', limit: 30, }).then(res => { diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 6057174ba..34d7b0e4e 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useInterval } from '@/scripts/use-interval.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; @@ -54,7 +54,7 @@ const key = ref(0); const tick = () => { if (props.userListId == null) return; - os.api('notes/user-list-timeline', { + misskeyApi('notes/user-list-timeline', { listId: props.userListId, }).then(res => { notes.value = res; diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts index 5239b7670..4c77465eb 100644 --- a/packages/frontend/src/ui/_common_/sw-inject.ts +++ b/packages/frontend/src/ui/_common_/sw-inject.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { api, post } from '@/os.js'; +import { post } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i, login } from '@/account.js'; import { getAccountFromId } from '@/scripts/get-account-from-id.js'; -import { mainRouter } from '@/router.js'; import { deepClone } from '@/scripts/clone.js'; +import { mainRouter } from '@/global/router/main.js'; export function swInject() { navigator.serviceWorker.addEventListener('message', async ev => { @@ -30,10 +31,10 @@ export function swInject() { // プッシュ通知から来たreply,renoteはtruncateBodyが通されているため、 // 完全なノートを取得しなおす if (props.reply) { - props.reply = await api('notes/show', { noteId: props.reply.id }); + props.reply = await misskeyApi('notes/show', { noteId: props.reply.id }); } if (props.renote) { - props.renote = await api('notes/show', { noteId: props.renote.id }); + props.renote = await misskeyApi('notes/show', { noteId: props.renote.id }); } return post(props); } diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index 3bb909798..959a135cc 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -52,11 +52,11 @@ import XCommon from './_common_/common.vue'; import { instanceName } from '@/config.js'; import { StickySidebar } from '@/scripts/sticky-sidebar.js'; import * as os from '@/os.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; +import { mainRouter } from '@/global/router/main.js'; const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 0df814fc8..c592d01fb 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -103,7 +103,6 @@ import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { defaultStore } from '@/store.js'; @@ -117,6 +116,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue'; import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; +import { mainRouter } from '@/global/router/main.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index 7cd1d6aee..5c927691d 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -19,6 +19,7 @@ import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -35,7 +36,7 @@ onMounted(() => { }); async function setAntenna() { - const antennas = await os.api('antennas/list'); + const antennas = await misskeyApi('antennas/list'); const { canceled, result: antenna } = await os.select({ title: i18n.ts.selectAntenna, items: antennas.map(x => ({ diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 95ed900f7..7293f82b2 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -26,6 +26,7 @@ import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -41,7 +42,7 @@ if (props.column.channelId == null) { } async function setChannel() { - const channels = await os.api('channels/my-favorites', { + const channels = await misskeyApi('channels/my-favorites', { limit: 100, }); const { canceled, result: channel } = await os.select({ @@ -60,7 +61,7 @@ async function setChannel() { async function post() { if (!channel.value || channel.value.id !== props.column.channelId) { - channel.value = await os.api('channels/show', { + channel.value = await misskeyApi('channels/show', { channelId: props.column.channelId, }); } diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index e68b7bba8..ae68029cd 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -7,7 +7,7 @@ import { throttle } from 'throttle-debounce'; import { markRaw } from 'vue'; import { notificationTypes } from 'misskey-js'; import { Storage } from '@/pizzax.js'; -import { api } from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { deepClone } from '@/scripts/clone.js'; type ColumnWidget = { @@ -70,7 +70,7 @@ export const loadDeck = async () => { let deck; try { - deck = await api('i/registry/get', { + deck = await misskeyApi('i/registry/get', { scope: ['client', 'deck', 'profiles'], key: deckStore.state.profile, }); @@ -95,7 +95,7 @@ export const loadDeck = async () => { // TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する export const saveDeck = throttle(1000, () => { - api('i/registry/set', { + misskeyApi('i/registry/set', { scope: ['client', 'deck', 'profiles'], key: deckStore.state.profile, value: { @@ -106,13 +106,13 @@ export const saveDeck = throttle(1000, () => { }); export async function getProfiles(): Promise { - return await api('i/registry/keys', { + return await misskeyApi('i/registry/keys', { scope: ['client', 'deck', 'profiles'], }); } export async function deleteProfile(key: string): Promise { - return await api('i/registry/remove', { + return await misskeyApi('i/registry/remove', { scope: ['client', 'deck', 'profiles'], key: key, }); diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 45ecc476e..a869e2743 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -19,6 +19,7 @@ import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -40,7 +41,7 @@ watch(withRenotes, v => { }); async function setList() { - const lists = await os.api('users/lists/list'); + const lists = await misskeyApi('users/lists/list'); const { canceled, result: list } = await os.select({ title: i18n.ts.selectList, items: lists.map(x => ({ diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index cd567040f..0ea6a7f23 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -24,10 +24,10 @@ import XColumn from './column.vue'; import { deckStore, Column } from '@/ui/deck/deck-store.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; import { getScrollContainer } from '@/scripts/scroll.js'; +import { mainRouter } from '@/global/router/main.js'; defineProps<{ column: Column; diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index 5fbd1389b..e5bffcc4e 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -19,6 +19,7 @@ import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -35,7 +36,7 @@ onMounted(() => { }); async function setRole() { - const roles = (await os.api('roles/list')).filter(x => x.isExplorable); + const roles = (await misskeyApi('roles/list')).filter(x => x.isExplorable); const { canceled, result: role } = await os.select({ title: i18n.ts.role, items: roles.map(x => ({ diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index f32f2de3d..b0a2aa35f 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only