diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.md b/.github/ISSUE_TEMPLATE/01_bug-report.md index 8734fc0c36..0fecce2ee8 100644 --- a/.github/ISSUE_TEMPLATE/01_bug-report.md +++ b/.github/ISSUE_TEMPLATE/01_bug-report.md @@ -22,7 +22,10 @@ First, in order to avoid duplicate Issues, please search to see if the problem y ## ๐Ÿคฌ Actual Behavior - + ## ๐Ÿ“ Steps to Reproduce diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..98f1d2e383 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,12 @@ +'โš™๏ธServer': +- packages/backend/**/* + +'๐Ÿ–ฅ๏ธClient': +- packages/client/**/* + +'๐ŸงชTest': +- cypress/**/* +- packages/backend/test/**/* + +'โ€ผ๏ธ wrong locales': +- any: ['locales/*.yml', '!locales/ja-JP.yml'] diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..fa4a58c3a9 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,16 @@ +name: "Pull Request Labeler" +on: + pull_request_target: + branches-ignore: + - 'l10n_develop' + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index da2c73a656..4e42fa9314 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,25 +1,39 @@ -name: Lint - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: true - - uses: actions/setup-node@v3 - with: - node-version: 16.x - cache: 'yarn' - cache-dependency-path: | - packages/backend/yarn.lock - packages/client/yarn.lock - - run: yarn install - - run: yarn lint +name: Lint + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + backend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: 'yarn' + cache-dependency-path: | + packages/backend/yarn.lock + - run: yarn install + - run: yarn --cwd ./packages/backend lint + + client: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: 'yarn' + cache-dependency-path: | + packages/client/yarn.lock + - run: yarn install + - run: yarn --cwd ./packages/client lint diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml new file mode 100644 index 0000000000..87af3a6ba6 --- /dev/null +++ b/.github/workflows/ok-to-test.yml @@ -0,0 +1,36 @@ +# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event +name: Ok To Test + +on: + issue_comment: + types: [created] + +jobs: + ok-to-test: + runs-on: ubuntu-latest + # Only run for PRs, not issue comments + if: ${{ github.event.issue.pull_request }} + steps: + # Generate a GitHub App installation access token from an App ID and private key + # To create a new GitHub App: + # https://developer.github.com/apps/building-github-apps/creating-a-github-app/ + # See app.yml for an example app manifest + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.DEPLOYBOT_APP_ID }} + private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} + + - name: Slash Command Dispatch + uses: peter-evans/slash-command-dispatch@v1 + env: + TOKEN: ${{ steps.generate_token.outputs.token }} + with: + token: ${{ env.TOKEN }} # GitHub App installation access token + # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work + reaction-token: ${{ secrets.GITHUB_TOKEN }} + issue-type: pull-request + commands: deploy + named-args: true + permission: write diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml new file mode 100644 index 0000000000..fd43bce9e6 --- /dev/null +++ b/.github/workflows/pr-preview-deploy.yml @@ -0,0 +1,95 @@ +# Run secret-dependent integration tests only after /deploy approval +on: + pull_request: + types: [opened, reopened, synchronize] + repository_dispatch: + types: [deploy-command] + +name: Deploy preview environment + +jobs: + # Repo owner has commented /deploy on a (fork-based) pull request + deploy-preview-environment: + runs-on: ubuntu-latest + if: + github.event_name == 'repository_dispatch' && + github.event.client_payload.slash_command.sha != '' && + contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) + steps: + - uses: actions/github-script@v5 + id: check-id + env: + number: ${{ github.event.client_payload.pull_request.number }} + job: ${{ github.job }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const { data: pull } = await github.rest.pulls.get({ + ...context.repo, + pull_number: process.env.number + }); + const ref = pull.head.sha; + + const { data: checks } = await github.rest.checks.listForRef({ + ...context.repo, + ref + }); + + const check = checks.check_runs.filter(c => c.name === process.env.job); + + return check[0].id; + + - uses: actions/github-script@v5 + env: + check_id: ${{ steps.check-id.outputs.result }} + details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.checks.update({ + ...context.repo, + check_run_id: process.env.check_id, + status: 'in_progress', + details_url: process.env.details_url + }); + + # Check out merge commit + - name: Fork based /deploy checkout + uses: actions/checkout@v2 + with: + ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' + + # + - name: Context + uses: okteto/context@latest + with: + token: ${{ secrets.OKTETO_TOKEN }} + + - name: Deploy preview environment + uses: ikuradon/deploy-preview@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo + timeout: 15m + + # Update check run called "integration-fork" + - uses: actions/github-script@v5 + id: update-check-run + if: ${{ always() }} + env: + # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run + conclusion: ${{ job.status }} + check_id: ${{ steps.check-id.outputs.result }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: result } = await github.rest.checks.update({ + ...context.repo, + check_run_id: process.env.check_id, + status: 'completed', + conclusion: process.env.conclusion + }); + + return result; diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml new file mode 100644 index 0000000000..c14c3db5c5 --- /dev/null +++ b/.github/workflows/pr-preview-destroy.yml @@ -0,0 +1,21 @@ +# file: .github/workflows/preview-closed.yaml +on: + pull_request: + types: + - closed + +name: Destroy preview environment + +jobs: + destroy-preview-environment: + runs-on: ubuntu-latest + steps: + - name: Context + uses: okteto/context@latest + with: + token: ${{ secrets.OKTETO_TOKEN }} + + - name: Destroy preview environment + uses: okteto/destroy-preview@latest + with: + name: pr-${{ github.event.number }}-syuilo diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d57d85c874..c32c82e2a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] services: postgres: @@ -57,7 +57,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x] + node-version: [18.x] browser: [chrome] services: @@ -103,7 +103,7 @@ jobs: - name: ALSA Env run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc - name: Cypress run - uses: cypress-io/github-action@v2 + uses: cypress-io/github-action@v4 with: install: false start: npm run start:test diff --git a/.node-version b/.node-version index bf79505bb8..7fd023741b 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v16.14.0 +v16.15.0 diff --git a/.okteto/okteto-pipeline.yml b/.okteto/okteto-pipeline.yml new file mode 100644 index 0000000000..e2996fbbc9 --- /dev/null +++ b/.okteto/okteto-pipeline.yml @@ -0,0 +1,6 @@ +build: + misskey: + args: + - NODE_ENV=development +deploy: + - helm upgrade --install misskey chart --set image=${OKTETO_BUILD_MISSKEY_IMAGE} --set url="https://misskey-$(kubectl config view --minify -o jsonpath='{..namespace}').cloud.okteto.net" --set environment=development diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9adb0d0697..42264548ea 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "editorconfig.editorconfig", "eg2.vscode-npm-script", "dbaeumer.vscode-eslint", - "johnsoncodehk.volar", + "Vue.volar", + "Vue.vscode-typescript-vue-plugin" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index dff67b40dc..eb16a1675d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## 12.x.x (unreleased) ### Improvements -- ### Bugfixes - @@ -10,11 +9,59 @@ You should also include the user name that made the change. --> +## 12.111.0 (2022/06/11) +### Improvements +- Supports Unicode Emoji 14.0 @mei23 +- ใƒ—ใƒƒใ‚ทใƒฅ้€š็Ÿฅใ‚’่ค‡ๆ•ฐใ‚ขใ‚ซใ‚ฆใƒณใƒˆๅฏพๅฟœใซ #7667 @tamaina +- ใƒ—ใƒƒใ‚ทใƒฅ้€š็Ÿฅใซใ‚ฏใƒชใƒƒใ‚ฏใ‚„actionใ‚’่จญๅฎš #7667 @tamaina +- ใƒ‰ใƒฉใ‚คใƒ–ใซ็”ปๅƒใƒ•ใ‚กใ‚คใƒซใ‚’ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ™ใ‚‹ใจใใ‚ชใƒชใ‚ธใƒŠใƒซ็”ปๅƒใ‚’็ ดๆฃ„ใ—ใฆwebpublicใฎใฟไฟๆŒใ™ใ‚‹ใ‚ชใƒ—ใ‚ทใƒงใƒณ @tamaina +- Server: always remove completed tasks of job queue @Johann150 +- Client: ใ‚ขใƒใ‚ฟใƒผใฎ่จญๅฎšใง็”ปๅƒใ‚’ใ‚ฏใƒญใƒƒใƒ—ใงใใ‚‹ใ‚ˆใ†ใซ @syuilo +- Client: make emoji stand out more on reaction button @Johann150 +- Client: display URL of QR code for TOTP registration @tamaina +- Client: render quote renote CWs as MFM @pixeldesu +- API: notifications/readใฏ้…ๅˆ—ใงใ‚‚ๅ—ใ‘ไป˜ใ‘ใ‚‹ใ‚ˆใ†ใซ #7667 @tamaina +- API: ใƒฆใƒผใ‚ถใƒผๆคœ็ดขใงใ€ใ‚ฏใ‚จใƒชใŒusernameใฎๆกไปถใ‚’ๆบ€ใŸใ™ๅ ดๅˆใฏusernameใ‚‚LIKEๆคœ็ดขใ™ใ‚‹ใ‚ˆใ†ใซ @tamaina +- MFM: Allow speed changes in all animated MFMs @Johann150 +- The theme color is now better validated. @Johann150 + Your own theme color may be unset if it was in an invalid format. + Admins should check their instance settings if in doubt. +- Perform port diagnosis at startup only when Listen fails @mei23 +- Rate limiting is now also usable for non-authenticated users. @Johann150 @mei23 + Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address. + +### Bugfixes +- Server: keep file order of note attachement @Johann150 +- Server: fix caching @Johann150 +- Server: fix missing foreign key for reports leading to reports page being unusable @Johann150 +- Server: fix internal in-memory caching @Johann150 +- Server: use correct order of attachments on notes @Johann150 +- Server: prevent crash when processing certain PNGs @syuilo +- Server: Fix unable to generate video thumbnails @mei23 +- Server: Fix `Cannot find module` issue @mei23 +- Federation: Add rel attribute to host-meta @mei23 +- Federation: add id for activitypub follows @Johann150 +- Federation: use `source` instead of `_misskey_content` @Johann150 +- Federation: ensure resolver does not fetch local resources via HTTP(S) @Johann150 +- Federation: correctly render empty note text @Johann150 +- Federation: Fix quote renotes containing no text being federated correctly @Johann150 +- Federation: remove duplicate br tag/newline @Johann150 +- Federation: add missing authorization checks @Johann150 +- Client: fix profile picture height in mentions @tamaina +- Client: fix abuse reports page to be able to show all reports @Johann150 +- Client: fix settings page @tamaina +- Client: fix profile tabs @futchitwo +- Client: fix popout URL @futchitwo +- Client: correctly handle MiAuth URLs with query string @sn0w +- Client: ใƒŽใƒผใƒˆ่ฉณ็ดฐใƒšใƒผใ‚ธใฎๆ–ฐใ—ใ„ใƒŽใƒผใƒˆใ‚’่กจ็คบใ™ใ‚‹ๆฉŸ่ƒฝใฎๅ‹•ไฝœใŒๆญฃใ—ใใชใ‚‹ใ‚ˆใ†ใซไฟฎๆญฃใ™ใ‚‹ @xianonn +- MFM: more animated functions support `speed` parameter @futchitwo +- MFM: limit large MFM @Johann150 + ## 12.110.1 (2022/04/23) ### Bugfixes - Fix GOP rendering @syuilo -- Improve performance of antenna, clip, and list @xianon +- Improve performance of antenna, clip, and list @xianonn ## 12.110.0 (2022/04/11) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a696bc5ceb..a37df3bdee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,11 @@ # Contribution guide We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project. -**โ„น๏ธ Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** -Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ -The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. -It will also allow the reader to use the translation tool of their preference if necessary. +> **Note** +> This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** +> Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ +> The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. +> It will also allow the reader to use the translation tool of their preference if necessary. ## Roadmap See [ROADMAP.md](./ROADMAP.md) @@ -16,6 +17,9 @@ Before creating an issue, please check the following: - Issues should only be used to feature requests, suggestions, and bug tracking. - Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3). +> **Warning** +> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. + ## Before implementation When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. @@ -62,6 +66,30 @@ Be willing to comment on the good points and not just the things you want fixed - Are there any omissions or gaps? - Does it check for anomalies? +## Deploy +The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment. +``` +/deploy sha= +``` +An actual domain will be assigned so you can test the federation. + +## Merge +For now, basically only @syuilo has the authority to merge PRs into develop because he is most familiar with the codebase. +However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor. + +## Release +### Release Instructions +1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) +2. Create a release PR. + - Into `master` from `develop` branch. + - The title must be in the format `Release: x.y.z`. + - `x.y.z` is the new version you are trying to release. +3. Deploy and perform a simple QA check. Also verify that the tests passed. +4. Merge it. +5. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) + - The target branch must be `master` + - The tag name must be the version + ## Localization (l10n) Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management. You can improve our translations with your Crowdin account. diff --git a/Dockerfile b/Dockerfile index e4959756e8..33d5faad12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM node:16.14.0-alpine3.15 AS base +FROM node:18.0.0-alpine3.15 AS base -ENV NODE_ENV=production +ARG NODE_ENV=production WORKDIR /misskey @@ -11,16 +11,16 @@ FROM base AS builder COPY . ./ RUN apk add --no-cache $BUILD_DEPS && \ - git submodule update --init && \ - yarn install && \ - yarn build && \ - rm -rf .git + git submodule update --init && \ + yarn install && \ + yarn build && \ + rm -rf .git FROM base AS runner RUN apk add --no-cache \ - ffmpeg \ - tini + ffmpeg \ + tini ENTRYPOINT ["/sbin/tini", "--"] @@ -31,5 +31,6 @@ COPY --from=builder /misskey/packages/backend/built ./packages/backend/built COPY --from=builder /misskey/packages/client/node_modules ./packages/client/node_modules COPY . ./ +ENV NODE_ENV=production CMD ["npm", "run", "migrateandstart"] diff --git a/README.md b/README.md index c7bc9ef219..c273270644 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,29 @@ -[![Misskey](https://github.com/misskey-dev/assets/blob/main/banner.png?raw=true)](https://join.misskey.page/) -
- -**๐ŸŒŽ A forever evolving, interplanetary microblogging platform. ๐Ÿš€** - -**Misskey** is a distributed microblogging platform with advanced features such as Reactions and a highly customizable UI. - -[Learn more](https://misskey-hub.net/) - + + Misskey logo + + +**๐ŸŒŽ **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! ๐Ÿš€** + --- -[โœจ Find an instance](https://misskey-hub.net/instances.html) -โ€ข -[๐Ÿ“ฆ Create your own instance](https://misskey-hub.net/docs/install.html) -โ€ข -[๐Ÿ› ๏ธ Contribute](./CONTRIBUTING.md) -โ€ข -[๐Ÿš€ Join the community](https://discord.gg/Wp8gVStHW3) + + find an instance + + create an instance + + + become a contributor + + + join the community + + + become a patron + --- -Become a Patron! -
@@ -30,139 +32,26 @@ ## โœจ Features - **ActivityPub support**\ - It is possible to interact with other software. +Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed! - **Reactions**\ - You can add "reactions" to each post, making it easy for you to express your feelings. +You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button. - **Drive**\ - An interface to manage uploaded files such as images, videos, sounds, etc. - You can also organize your favorite content into folders, making it easy to share again. +With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made! - **Rich Web UI**\ - Misskey has a rich WebUI by default. - It is highly customizable by flexibly changing the layout and installing various widgets and themes. - Furthermore, plug-ins can be created using AiScript, a original programming language. -- and more... + Misskey has a rich and easy to use Web UI! + It is highly customizable, from changing the layout and adding widgets to making custom themes. + Furthermore, plugins can be created using AiScript, an original programming language. +- And much more...
+## Documentation + +Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it. + ## Sponsors
RSS3
- -## Backers - - - - - - - - - - - - - - - -
Roujo Oliver Maximilian Seidelweepjp kiritan ใฟใชใ—ใพ Eduardo Quiros
Roujo Oliver Maximilian Seidelweepjp kiritan ใฟใชใ—ใพ Eduardo Quiros
- - - - - - - - - - - - - - - - - - - - -
NesakkoDemogrognardLiaizon Wakestmkatze kabo2468y AureoleArk osapon ่ฆ‹ๅฝ“ใ‹ใชใฟ Wataru Manji (manji0)
NesakkoDemogrognardLiaizon Wakestmkatze kabo2468y AureoleArk osapon ่ฆ‹ๅฝ“ใ‹ใชใฟ Wataru Manji (manji0)
- - - - - - - - - - - - - - - - - - -
YuzuRyo61 mewl hayabusaS YTakumi Sugitasikyosyounin YUKIMOCHIxianontotokoro
YuzuRyo61 mewl hayabusaS YTakumi Sugitasikyosyounin YUKIMOCHIxianontotokoro
- - - - - - - - - - - - - - - - - - - - - - - - -
sheeta.s motcha axtuki1 Satsuki Yanagitakimura aqz tamainanafuchoco Atsuko TominaganatalieEBISUMEnoellabo
sheeta.s motcha axtuki1 Satsuki Yanagitakimura aqz tamainanafuchoco Atsuko TominaganatalieEBISUMEnoellabo
- - - - - - - - - - - - - - - - - - -
CG Hekovic uroco @99Chandler Nokotaro Takedanenohi Efertone Takashi Shibuya
CG Hekovic uroco @99Chandler Nokotaro Takedanenohi Efertone Takashi Shibuya
- -**Last updated:** Sun, 26 Jul 2020 07:00:10 UTC - - -[backer-url]: #backers -[backer-badge]: https://opencollective.com/misskey/backers/badge.svg -[backers-image]: https://opencollective.com/misskey/backers.svg -[sponsor-url]: #sponsors -[sponsor-badge]: https://opencollective.com/misskey/sponsors/badge.svg -[sponsors-image]: https://opencollective.com/misskey/sponsors.svg -[support-url]: https://opencollective.com/misskey#support - -[syuilo-link]: https://syuilo.com -[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 diff --git a/assets/title_float.svg b/assets/title_float.svg new file mode 100644 index 0000000000..43205ac1c4 --- /dev/null +++ b/assets/title_float.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000000..8f31cf7fb4 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: misskey +version: 0.0.0 diff --git a/chart/files/default.yml b/chart/files/default.yml new file mode 100644 index 0000000000..a9ef22f424 --- /dev/null +++ b/chart/files/default.yml @@ -0,0 +1,165 @@ +#โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +# Misskey configuration +#โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +# โ”Œโ”€โ”€โ”€โ”€โ”€โ” +#โ”€โ”€โ”€โ”˜ URL โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Final accessible URL seen by a user. +# url: https://example.tld/ + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# URL SETTINGS AFTER THAT! + +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +#โ”€โ”€โ”€โ”˜ Port and TLS settings โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# +# Misskey supports two deployment options for public. +# + +# Option 1: With Reverse Proxy +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to setup reverse proxy. (eg. nginx) +# You do not define 'https' section. + +# Option 2: Standalone +# +# +- https://example.tld/ -+ +# +------+ | +---------------+ | +# | User | ---> | | Misskey (443) | | +# +------+ | +---------------+ | +# +------------------------+ +# +# You need to run Misskey as root. +# You need to set Certificate in 'https' section. + +# To use option 1, uncomment below line. +port: 3000 # A port that your Misskey server should listen. + +# To use option 2, uncomment below lines. +#port: 443 + +#https: +# # path for certification +# key: /etc/letsencrypt/live/example.tld/privkey.pem +# cert: /etc/letsencrypt/live/example.tld/fullchain.pem + +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +#โ”€โ”€โ”€โ”˜ PostgreSQL configuration โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +db: + host: localhost + port: 5432 + + # Database name + db: misskey + + # Auth + user: example-misskey-user + pass: example-misskey-pass + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +#โ”€โ”€โ”€โ”˜ Redis configuration โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +redis: + host: localhost + port: 6379 + #pass: example-pass + #prefix: example-prefix + #db: 1 + +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +#โ”€โ”€โ”€โ”˜ Elasticsearch configuration โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +#elasticsearch: +# host: localhost +# port: 9200 +# ssl: false +# user: +# pass: + +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +#โ”€โ”€โ”€โ”˜ ID generation โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. + +# Available methods: +# aid ... Short, Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + +id: "aid" +# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +#โ”€โ”€โ”€โ”˜ Other configuration โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Whether disable HSTS +#disableHsts: true + +# Number of worker processes +#clusterLimit: 1 + +# Job concurrency per worker +# deliverJobConcurrency: 128 +# inboxJobConcurrency: 16 + +# Job rate limiter +# deliverJobPerSec: 128 +# inboxJobPerSec: 16 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# Syslog option +#syslog: +# host: localhost +# port: 514 + +# Proxy for HTTP/HTTPS +#proxy: http://127.0.0.1:3128 + +#proxyBypassHosts: [ +# 'example.com', +# '192.0.2.8' +#] + +# Proxy for SMTP/SMTPS +#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT +#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 +#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 + +# Media Proxy +#mediaProxy: https://example.com/proxy + +# Sign to ActivityPub GET request (default: false) +#signToActivityPubGet: true + +#allowedPrivateNetworks: [ +# '127.0.0.1/32' +#] + +# Upload or download file size limits (bytes) +#maxFileSize: 262144000 diff --git a/chart/templates/ConfigMap.yml b/chart/templates/ConfigMap.yml new file mode 100644 index 0000000000..37c25e0864 --- /dev/null +++ b/chart/templates/ConfigMap.yml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "misskey.fullname" . }}-configuration +data: + default.yml: |- + {{ .Files.Get "files/default.yml"|nindent 4 }} + url: {{ .Values.url }} diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml new file mode 100644 index 0000000000..d16aece915 --- /dev/null +++ b/chart/templates/Deployment.yml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "misskey.fullname" . }} + labels: + {{- include "misskey.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "misskey.selectorLabels" . | nindent 6 }} + replicas: 1 + template: + metadata: + labels: + {{- include "misskey.selectorLabels" . | nindent 8 }} + spec: + containers: + - name: misskey + image: {{ .Values.image }} + env: + - name: NODE_ENV + value: {{ .Values.environment }} + volumeMounts: + - name: {{ include "misskey.fullname" . }}-configuration + mountPath: /misskey/.config + readOnly: true + ports: + - containerPort: 3000 + - name: postgres + image: postgres:14-alpine + env: + - name: POSTGRES_USER + value: "example-misskey-user" + - name: POSTGRES_PASSWORD + value: "example-misskey-pass" + - name: POSTGRES_DB + value: "misskey" + ports: + - containerPort: 5432 + - name: redis + image: redis:alpine + ports: + - containerPort: 6379 + volumes: + - name: {{ include "misskey.fullname" . }}-configuration + configMap: + name: {{ include "misskey.fullname" . }}-configuration diff --git a/chart/templates/Service.yml b/chart/templates/Service.yml new file mode 100644 index 0000000000..3209581298 --- /dev/null +++ b/chart/templates/Service.yml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "misskey.fullname" . }} + annotations: + dev.okteto.com/auto-ingress: "true" +spec: + type: ClusterIP + ports: + - port: 3000 + protocol: TCP + name: http + selector: + {{- include "misskey.selectorLabels" . | nindent 4 }} diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000000..a5a2499f3f --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "misskey.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "misskey.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "misskey.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "misskey.labels" -}} +helm.sh/chart: {{ include "misskey.chart" . }} +{{ include "misskey.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "misskey.selectorLabels" -}} +app.kubernetes.io/name: {{ include "misskey.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "misskey.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "misskey.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/chart/values.yml b/chart/values.yml new file mode 100644 index 0000000000..a7031538a9 --- /dev/null +++ b/chart/values.yml @@ -0,0 +1,3 @@ +url: https://example.tld/ +image: okteto.dev/misskey +environment: production diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 0000000000..e390c41a54 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'cypress' + +export default defineConfig({ + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + return require('./cypress/plugins/index.js')(on, config) + }, + baseUrl: 'http://localhost:61812', + }, +}) diff --git a/cypress.json b/cypress.json deleted file mode 100644 index e858e480b0..0000000000 --- a/cypress.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "baseUrl": "http://localhost:61812" -} diff --git a/cypress/integration/basic.js b/cypress/e2e/basic.cy.js similarity index 75% rename from cypress/integration/basic.js rename to cypress/e2e/basic.cy.js index 7d27b649f4..eb5195c4b2 100644 --- a/cypress/integration/basic.js +++ b/cypress/e2e/basic.cy.js @@ -1,8 +1,6 @@ describe('Before setup instance', () => { beforeEach(() => { - cy.request('POST', '/api/reset-db').as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); + cy.resetState(); }); afterEach(() => { @@ -32,15 +30,10 @@ describe('Before setup instance', () => { describe('After setup instance', () => { beforeEach(() => { - cy.request('POST', '/api/reset-db').as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); + cy.resetState(); // ใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นๅˆๆœŸใ‚ปใƒƒใƒˆใ‚ขใƒƒใƒ— - cy.request('POST', '/api/admin/accounts/create', { - username: 'admin', - password: 'pass', - }).its('body').as('admin'); + cy.registerUser('admin', 'pass', true); }); afterEach(() => { @@ -70,21 +63,13 @@ describe('After setup instance', () => { describe('After user signup', () => { beforeEach(() => { - cy.request('POST', '/api/reset-db').as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); + cy.resetState(); // ใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นๅˆๆœŸใ‚ปใƒƒใƒˆใ‚ขใƒƒใƒ— - cy.request('POST', '/api/admin/accounts/create', { - username: 'admin', - password: 'pass', - }).its('body').as('admin'); + cy.registerUser('admin', 'pass', true); // ใƒฆใƒผใ‚ถใƒผไฝœๆˆ - cy.request('POST', '/api/signup', { - username: 'alice', - password: 'alice1234', - }).its('body').as('alice'); + cy.registerUser('alice', 'alice1234'); }); afterEach(() => { @@ -129,31 +114,15 @@ describe('After user signup', () => { describe('After user singed in', () => { beforeEach(() => { - cy.request('POST', '/api/reset-db').as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); + cy.resetState(); // ใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นๅˆๆœŸใ‚ปใƒƒใƒˆใ‚ขใƒƒใƒ— - cy.request('POST', '/api/admin/accounts/create', { - username: 'admin', - password: 'pass', - }).its('body').as('admin'); + cy.registerUser('admin', 'pass', true); // ใƒฆใƒผใ‚ถใƒผไฝœๆˆ - cy.request('POST', '/api/signup', { - username: 'alice', - password: 'alice1234', - }).its('body').as('alice'); + cy.registerUser('alice', 'alice1234'); - cy.visit('/'); - - cy.intercept('POST', '/api/signin').as('signin'); - - cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-username] input').type('alice'); - cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); - - cy.wait('@signin').as('signedIn'); + cy.login('alice', 'alice1234'); }); afterEach(() => { @@ -163,12 +132,10 @@ describe('After user singed in', () => { }); it('successfully loads', () => { - cy.visit('/'); + cy.get('[data-cy-open-post-form]').should('be.visible'); }); it('note', () => { - cy.visit('/'); - cy.get('[data-cy-open-post-form]').click(); cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); cy.get('[data-cy-open-post-form-submit]').click(); diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.js new file mode 100644 index 0000000000..56ad95ee94 --- /dev/null +++ b/cypress/e2e/widgets.cy.js @@ -0,0 +1,65 @@ +describe('After user signed in', () => { + beforeEach(() => { + cy.resetState(); + cy.viewport('macbook-16'); + + // ใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นๅˆๆœŸใ‚ปใƒƒใƒˆใ‚ขใƒƒใƒ— + cy.registerUser('admin', 'pass', true); + + // ใƒฆใƒผใ‚ถใƒผไฝœๆˆ + cy.registerUser('alice', 'alice1234'); + + cy.login('alice', 'alice1234'); + }); + + afterEach(() => { + // ใƒ†ใ‚นใƒˆ็ต‚ไบ†็›ดๅ‰ใซใƒšใƒผใ‚ธ้ท็งปใ™ใ‚‹ใ‚ˆใ†ใชใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น(ไพ‹ใˆใฐใ‚ขใ‚ซใ‚ฆใƒณใƒˆไฝœๆˆ)ใ ใจใ€ใŸใถใ‚“Cypressใฎใƒใ‚ฐใงใƒ–ใƒฉใ‚ฆใ‚ถใฎๅ†…ๅฎนใŒๆฌกใฎใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚นใซๅผ•ใ็ถ™ใŒใ‚Œใฆใ—ใพใ†(ไพ‹ใˆใฐใ‚ขใ‚ซใ‚ฆใƒณใƒˆใŒไฝœๆˆใ—็ต‚ใ‚ใฃใŸๆฎต้šŽใ‹ใ‚‰ใƒ†ใ‚นใƒˆใŒๅง‹ใพใ‚‹)ใ€‚ + // waitใ‚’ๅ…ฅใ‚Œใ‚‹ใ“ใจใงใใ‚Œใ‚’้˜ฒๆญขใงใใ‚‹ + cy.wait(1000); + }); + + it('widget edit toggle is visible', () => { + cy.get('.mk-widget-edit').should('be.visible'); + }); + + it('widget select should be visible in edit mode', () => { + cy.get('.mk-widget-edit').click(); + cy.get('.mk-widget-select').should('be.visible'); + }); + + it('first widget should be removed', () => { + cy.get('.mk-widget-edit').click(); + cy.get('.customize-container:first-child .remove._button').click(); + cy.get('.customize-container').should('have.length', 2); + }); + + function buildWidgetTest(widgetName) { + it(`${widgetName} widget should get added`, () => { + cy.get('.mk-widget-edit').click(); + cy.get('.mk-widget-select select').select(widgetName, { force: true }); + cy.get('.bg._modalBg.transparent').click({ multiple: true, force: true }); + cy.get('.mk-widget-add').click({ force: true }); + cy.get(`.mkw-${widgetName}`).should('exist'); + }); + } + + buildWidgetTest('memo'); + buildWidgetTest('notifications'); + buildWidgetTest('timeline'); + buildWidgetTest('calendar'); + buildWidgetTest('rss'); + buildWidgetTest('trends'); + buildWidgetTest('clock'); + buildWidgetTest('activity'); + buildWidgetTest('photos'); + buildWidgetTest('digitalClock'); + buildWidgetTest('federation'); + buildWidgetTest('postForm'); + buildWidgetTest('slideshow'); + buildWidgetTest('serverMetric'); + buildWidgetTest('onlineUsers'); + buildWidgetTest('jobQueue'); + buildWidgetTest('button'); + buildWidgetTest('aiscript'); + buildWidgetTest('aichan'); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 119ab03f7c..95bfcf6855 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -23,3 +23,33 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + +Cypress.Commands.add('resetState', () => { + cy.window(win => { + win.indexedDB.deleteDatabase('keyval-store'); + }); + cy.request('POST', '/api/reset-db').as('reset'); + cy.get('@reset').its('status').should('equal', 204); + cy.reload(true); +}); + +Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => { + const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup'; + + cy.request('POST', route, { + username: username, + password: password, + }).its('body').as(username); +}); + +Cypress.Commands.add('login', (username, password) => { + cy.visit('/'); + + cy.intercept('POST', '/api/signin').as('signin'); + + cy.get('[data-cy-signin]').click(); + cy.get('[data-cy-signin-username] input').type(username); + cy.get('[data-cy-signin-password] input').type(`${password}{enter}`); + + cy.wait('@signin').as('signedIn'); +}); diff --git a/cypress/support/index.js b/cypress/support/e2e.js similarity index 100% rename from cypress/support/index.js rename to cypress/support/e2e.js diff --git a/docker-compose.yml b/docker-compose.yml index e1d51668a7..0bf17a5557 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - redis # - es ports: - - "127.0.0.1:3000:3000" + - "3000:3000" networks: - internal_network - external_network diff --git a/gulpfile.js b/gulpfile.js index b7aa4e328e..90f8ebaabe 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -37,7 +37,6 @@ gulp.task('copy:client:locales', cb => { gulp.task('build:backend:script', () => { return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js']) - .pipe(replace('VERSION', JSON.stringify(meta.version))) .pipe(replace('LANGS', JSON.stringify(Object.keys(locales)))) .pipe(terser({ toplevel: true diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index f3f8b45777..3bd8f1e506 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -141,7 +141,7 @@ flagAsBotDescription: "ูุนู‘ู„ ู‡ุฐุง ุงู„ุฎูŠุงุฑ ุฅุฐุง ูƒุงู† ู‡ุฐุง ุงู„ุญ flagAsCat: "ุนู„ู‘ู… ู‡ุฐุง ุงู„ุญุณุงุจ ูƒุญุณุงุจ ู‚ุท" flagAsCatDescription: "ูุนู‘ู„ ู‡ุฐุง ุงู„ุฎูŠุงุฑ ู„ูˆุถุน ุนู„ุงู…ุฉ ุนู„ู‰ ุงู„ุญุณุงุจ ู„ุชูˆุถูŠุญ ุฃู†ู‡ ุญุณุงุจ ู‚ุท." flagShowTimelineReplies: "ุฃุธู‡ุฑ ุงู„ุชุนู„ูŠู‚ุงุช ููŠ ุงู„ุฎูŠุท ุงู„ุฒู…ู†ูŠ" -flagShowTimelineRepliesDescription: "ูŠุธู‡ุฑ ุงู„ุฑุฏูˆุฏ ููŠ ุงู„ุฎุท ุงู„ุฒู…ู†ูŠ" +flagShowTimelineRepliesDescription: "ูŠุธู‡ุฑ ุงู„ุฑุฏูˆุฏ ููŠ ุงู„ุฎูŠุท ุงู„ุฒู…ู†ูŠ" autoAcceptFollowed: "ุงู‚ุจู„ ุทู„ุจุงุช ุงู„ู…ุชุงุจุนุฉ ุชู„ู‚ุงุฆูŠุง ู…ู† ุงู„ุญุณุงุจุงุช ุงู„ู…ุชุงุจูŽุนุฉ" addAccount: "ุฃุถู ุญุณุงุจุงู‹" loginFailed: "ูุดู„ ุงู„ูˆู„ูˆุฌ" @@ -312,12 +312,12 @@ dayX: "{day}" monthX: "{month}" yearX: "{year}" pages: "ุงู„ุตูุญุงุช" -integration: "ุฏู…ุฌ" +integration: "ุงู„ุชูƒุงู…ู„" connectService: "ุงุชุตู„" disconnectService: "ุงู‚ุทุน ุงู„ุงุชุตุงู„" enableLocalTimeline: "ุชูุนูŠู„ ุงู„ุฎูŠุท ุงู„ู…ุญู„ูŠ" enableGlobalTimeline: "ุชูุนูŠู„ ุงู„ุฎูŠุท ุงู„ุฒู…ู†ูŠ ุงู„ุดุงู…ู„" -disablingTimelinesInfo: "ุณูŠุชู…ูƒู† ุงู„ู…ุฏูŠุฑูˆู† ูˆุงู„ู…ุดุฑููˆู† ู…ู† ุงู„ูˆุตูˆู„ ุฅู„ู‰ ูƒู„ ุงู„ุฎุทูˆุท ุงู„ุฒู…ู†ูŠุฉ ุญุชู‰ ูˆุฅู† ู„ู… ุชูุนู‘ู„." +disablingTimelinesInfo: "ุณูŠุชู…ูƒู† ุงู„ู…ุฏูŠุฑูˆู† ูˆุงู„ู…ุดุฑููˆู† ู…ู† ุงู„ูˆุตูˆู„ ุฅู„ู‰ ูƒู„ ุงู„ุฎูŠูˆุท ุงู„ุฒู…ู†ูŠุฉ ุญุชู‰ ูˆุฅู† ู„ู… ุชูุนู‘ู„." registration: "ุฅู†ุดุงุก ุญุณุงุจ" enableRegistration: "ุชูุนูŠู„ ุฅู†ุดุงุก ุงู„ุญุณุงุจุงุช ุงู„ุฌุฏูŠุฏุฉ" invite: "ุฏุนูˆุฉ" @@ -532,6 +532,7 @@ poll: "ุงุณุชุทู„ุงุน ุฑุฃูŠ" useCw: "ุฅุฎูุงุก ุงู„ู…ุญุชูˆู‰" enablePlayer: "ุงูุชุญ ู…ุดุบู„ ุงู„ููŠุฏูŠูˆ" disablePlayer: "ุฃุบู„ู‚ ู…ุดุบู„ ุงู„ููŠุฏูŠูˆ" +expandTweet: "ูˆุณู‘ุน ุงู„ุชุบุฑูŠุฏุฉ" themeEditor: "ู…ุตู…ู… ุงู„ู‚ูˆุงู„ุจ" description: "ุงู„ูˆุตู" describeFile: "ุฃุถู ุชุนู„ูŠู‚ู‹ุง ุชูˆุถูŠุญูŠู‹ุง" @@ -635,6 +636,7 @@ yes: "ู†ุนู…" no: "ู„ุง" driveFilesCount: "ุนุฏุฏ ุงู„ู…ู„ูุงุช ููŠ ู‚ุฑุต ุงู„ุชุฎุฒูŠู†" driveUsage: "ุงู„ู…ุณุชุบู„ ู…ู† ู‚ุฑุต ุงู„ุชุฎุฒูŠู†" +noCrawle: "ุงุฑูุถ ูู‡ุฑุณุฉ ุฒุงุญู ุงู„ูˆูŠุจ" noCrawleDescription: "ูŠุทู„ุจ ู…ู† ู…ุญุฑูƒุงุช ุงู„ุจุญุซ ุฃู„ู‘ุง ูŠููู‡ุฑุณูˆุง ู…ู„ููƒ ุงู„ุดุฎุตูŠ ูˆู…ู„ุงุญุธุงุช ูˆุตูุญุงุชูƒ ูˆู…ุง ุดุงุจู‡." alwaysMarkSensitive: "ุนู„ู‘ู… ุงูุชุฑุงุถูŠู‹ุง ุฌู…ูŠุน ู…ู„ุงุญุธุงุชูŠ ูƒุฐุงุช ู…ุญุชูˆู‰ ุญุณุงุณ" loadRawImages: "ุญู…ู‘ู„ ุงู„ุตูˆุฑ ุงู„ุฃุตู„ูŠุฉ ุจุฏู„ู‹ุง ู…ู† ุงู„ู…ุตุบุฑุงุช" @@ -878,9 +880,11 @@ _mfm: center: "ูˆุณุท" centerDescription: "ูŠู…ุฑูƒุฒ ุงู„ู…ุญุชูˆู‰ ููŠ ุงู„ูˆูŽุณูŽุท." quote: "ุงู‚ุชุจุณ" + quoteDescription: "ูŠุนุฑุถ ุงู„ู…ุญุชูˆู‰ ูƒุงู‚ุชุจุงุณ" emoji: "ุฅูŠู…ูˆุฌูŠ ู…ุฎุตุต" emojiDescription: "ุฅุญุงุทุฉ ุงุณู… ุงู„ุฅูŠู…ูˆุฌูŠ ุจู†ู‚ุทุชูŠ ุชูุณูŠุฑ ุณูŠุณุชุจุฏู„ู‡ ุจุตูˆุฑุฉ ุงู„ุฅูŠู…ูˆุฌูŠ." search: "ุงู„ุจุญุซ" + searchDescription: "ูŠุนุฑุถ ู†ุตู‹ุง ููŠ ุตู†ุฏูˆู‚ ุงู„ุจุญุซ" flip: "ุงู‚ู„ุจ" flipDescription: "ูŠู‚ู„ุจ ุงู„ู…ุญุชูˆู‰ ุนู…ูˆุฏูŠู‹ุง ุฃูˆ ุฃูู‚ูŠู‹ุง" jelly: "ุชุฃุซูŠุฑ (ู‡ู„ุงู…)" @@ -1003,7 +1007,6 @@ _sfx: antenna: "ุงู„ู‡ูˆุงุฆูŠุงุช" channel: "ุฅุดุนุงุฑุงุช ุงู„ู‚ู†ุงุช" _ago: - unknown: "ู…ุฌู‡ูˆู„" future: "ุงู„ู…ุณุชู‚ุจูŽู„" justNow: "ุงู„ู„ุญุธุฉ" secondsAgo: "ู…ู†ุฐ {n} ุซูˆุงู†ู" @@ -1030,12 +1033,12 @@ _tutorial: step3_3: "ุงู…ู„ุฃ ุงู„ู†ู…ูˆุฐุฌ ูˆุงู†ู‚ุฑ ุงู„ุฒุฑู‘ ุงู„ู…ูˆุฌูˆุฏ ููŠ ุฃุนู„ู‰ ุงู„ูŠู…ูŠู† ู„ู„ุฅุฑุณุงู„." step3_4: "ู„ูŠุณ ู„ุฏูŠูƒ ู…ุง ุชู‚ูˆู„ู‡ุŸ ุฅุฐุง ุงูƒุชุจ \"ุจุฏุฃุชู ุงุณุชุฎุฏู… ู…ูŠุณูƒูŠ\"." step4_1: "ู‡ู„ ู†ุดุฑุช ู…ู„ุงุญุธุชูƒ ุงู„ุฃูˆู„ู‰ุŸ" - step4_2: "ู…ุฑุญู‰! ูŠู…ูƒู†ูƒ ุงู„ุขู† ุฑุคูŠุฉ ู…ู„ุงุญุธุชูƒ ููŠ ุงู„ุฎุท ุงู„ุฒู…ู†ูŠ." - step5_1: "ูˆุงู„ุขู†ุŒ ู„ู†ุฌุนู„ ุงู„ุฎุท ุงู„ุฒู…ู†ูŠ ุฃูƒุซุฑ ุญูŠูˆูŠุฉ ูˆุฐู„ูƒ ุจู…ุชุงุจุนุฉ ุจุนุถ ุงู„ู…ุณุชุฎุฏู…ูŠู†." + step4_2: "ู…ุฑุญู‰! ูŠู…ูƒู†ูƒ ุงู„ุขู† ุฑุคูŠุฉ ู…ู„ุงุญุธุชูƒ ููŠ ุงู„ุฎูŠุท ุงู„ุฒู…ู†ูŠ." + step5_1: "ูˆุงู„ุขู†ุŒ ู„ู†ุฌุนู„ ุงู„ุฎูŠุท ุงู„ุฒู…ู†ูŠ ุฃูƒุซุฑ ุญูŠูˆูŠุฉ ูˆุฐู„ูƒ ุจู…ุชุงุจุนุฉ ุจุนุถ ุงู„ู…ุณุชุฎุฏู…ูŠู†." step5_2: "ุชุนุฑุถ ุตูุญุฉ {features} ุงู„ู…ู„ุงุญุธุงุช ุงู„ู…ุชุฏุงูˆู„ุฉ ููŠ ู‡ุฐุง ุงู„ู…ุซูŠู„ ูˆูŠุชูŠุญ ู„ูƒ {Explore} ุงู„ุนุซูˆุฑ ุนู„ู‰ ุงู„ู…ุณุชุฎุฏู…ูŠู† ุงู„ุฑุงุฆุฏูŠู†. ุงุนุซุฑ ุนู„ู‰ ุงู„ุฃุดุฎุงุต ุงู„ุฐูŠู† ูŠุซูŠุฑูˆู† ุฅู‡ุชู…ุงู…ูƒ ูˆุชุงุจุนู‡ู…!" step5_3: "ู„ู…ุชุงุจุนุฉ ู…ุณุชุฎุฏู…ูŠู† ุงุฏุฎู„ ู…ู„ูู‡ู… ุงู„ุดุฎุตูŠ ุจุงู„ู†ู‚ุฑ ุนู„ู‰ ุตูˆุฑุชู‡ู… ุงู„ุดุฎุตูŠุฉ ุซู… ุงุถุบุท ุฒุฑ 'ุชุงุจุน'." step5_4: "ุฅุฐุง ูƒุงู† ู„ุฏู‰ ุงู„ู…ุณุชุฎุฏู… ุฑู…ุฒ ู‚ูู„ ุจุฌูˆุงุฑ ุงุณู…ู‡ ุŒ ูˆุฌุจ ุนู„ูŠูƒ ุงู†ุชุธุงุฑู‡ ู„ูŠู‚ุจู„ ุทู„ุจ ุงู„ู…ุชุงุจุนุฉ ูŠุฏูˆูŠู‹ุง." - step6_1: "ุงู„ุขู† ุณุชุชู…ูƒู† ู…ู† ุฑุคูŠุฉ ู…ู„ุงุญุธุงุช ุงู„ู…ุณุชุฎุฏู…ูŠู† ุงู„ู…ุชุงุจูŽุนูŠู† ููŠ ุงู„ุฎุท ุงู„ุฒู…ู†ูŠ." + step6_1: "ุงู„ุขู† ุณุชุชู…ูƒู† ู…ู† ุฑุคูŠุฉ ู…ู„ุงุญุธุงุช ุงู„ู…ุณุชุฎุฏู…ูŠู† ุงู„ู…ุชุงุจูŽุนูŠู† ููŠ ุงู„ุฎูŠุท ุงู„ุฒู…ู†ูŠ." step6_2: "ูŠู…ูƒู†ูƒ ุงู„ุชูุงุนู„ ุจุณุฑุนุฉ ู…ุน ุงู„ู…ู„ุงุญุธุงุช ุนู† ุทุฑูŠู‚ ุฅุถุงูุฉ \"ุชูุงุนู„\"." step6_3: "ู„ุฅุถุงูุฉ ุชูุงุนู„ ู„ู…ู„ุงุญุธุฉ ุŒ ุงู†ู‚ุฑ ููˆู‚ ุนู„ุงู…ุฉ \"+\" ุฃุณูู„ ู„ู„ู…ู„ุงุญุธุฉ ูˆุงุฎุชุฑ ุงู„ุฅูŠู…ูˆุฌูŠ ุงู„ู…ุทู„ูˆุจ." step7_1: "ู…ุจุงุฑูƒ ! ุฃู†ู‡ูŠุช ุงู„ุฏูˆุฑุฉ ุงู„ุชุนู„ูŠู…ูŠุฉ ุงู„ุฃุณุงุณูŠุฉ ู„ุงุณุชุฎุฏุงู… ู…ูŠุณูƒูŠ." @@ -1201,8 +1204,13 @@ _charts: _instanceCharts: requests: "ุงู„ุทู„ุจุงุช" users: "ุชุจุงูŠู† ุนุฏุฏ ุงู„ู…ุณุชุฎุฏู…ูŠู†" + usersTotal: "ุชุจุงูŠู† ุนุฏุฏ ุงู„ู…ุณุชุฎุฏู…ูŠู†" notes: "ุชุจุงูŠู† ุนุฏุฏ ุงู„ู…ู„ุงุญุธุงุช" + notesTotal: "ุชุจุงูŠู† ุนุฏุฏ ุงู„ู…ู„ุงุญุธุงุช" + ff: "ุชุจุงูŠู† ุนุฏุฏ ุญุณุงุจุงุช ุงู„ู…ุชุงุจูŽุนุฉ/ุงู„ู…ุชุงุจูุนุฉ" + ffTotal: "ุชุจุงูŠู† ุนุฏุฏ ุญุณุงุจุงุช ุงู„ู…ุชุงุจูŽุนุฉ/ุงู„ู…ุชุงุจูุนุฉ" files: "ุชุจุงูŠู† ุนุฏุฏ ุงู„ู…ู„ูุงุช" + filesTotal: "ุชุจุงูŠู† ุนุฏุฏ ุงู„ู…ู„ูุงุช" _timelines: home: "ุงู„ุฑุฆูŠุณูŠ" local: "ุงู„ู…ุญู„ูŠ" @@ -1321,6 +1329,7 @@ _pages: random: "ุนุดูˆุงุฆูŠ" value: "ุงู„ู‚ูŠู…" fn: "ุฏูˆุงู„" + text: "ุฅุฌุฑุงุกุงุช ุนู„ู‰ ุงู„ู†ุตูˆุต" convert: "ุชุญูˆูŠู„" list: "ุงู„ู‚ูˆุงุฆู…" blocks: @@ -1501,6 +1510,10 @@ _notification: followRequestAccepted: "ุทู„ุจุงุช ุงู„ู…ุชุงุจุนุฉ ุงู„ู…ู‚ุจูˆู„ุฉ" groupInvited: "ุฏุนูˆุงุช ุงู„ูุฑูŠู‚" app: "ุฅุดุนุงุฑุงุช ุงู„ุชุทุจูŠู‚ุงุช ุงู„ู…ุฑุชุจุทุฉ" + _actions: + followBack: "ุชุงุจุนูƒ ุจุงู„ู…ุซู„" + reply: "ุฑุฏ" + renote: "ุฃุนุฏ ุงู„ู†ุดุฑ" _deck: alwaysShowMainColumn: "ุฃุธู‡ุฑ ุงู„ุนู…ูˆุฏ ุงู„ุฑุฆูŠุณูŠ ุฏุงุฆู…ู‹ุง" columnAlign: "ุญุงุฐู ุงู„ุฃุนู…ุฏุฉ" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index b2ba236fd5..d7753b6dcf 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -831,11 +831,18 @@ themeColor: "เฆฅเฆฟเฆฎเง‡เฆฐ เฆฐเฆ‚" size: "เฆ†เฆ•เฆพเฆฐ" numberOfColumn: "เฆ•เฆฒเฆพเฆฎเง‡เฆฐ เฆธเฆ‚เฆ–เงเฆฏเฆพ" searchByGoogle: "เฆ—เงเฆ—เฆฒ" +instanceDefaultLightTheme: "เฆ‡เฆจเงเฆธเฆŸเงเฆฏเฆพเฆจเงเฆธเง‡เฆฐ เฆกเฆฟเฆซเฆฒเงเฆŸ เฆฒเฆพเฆ‡เฆŸ เฆฅเฆฟเฆฎ" +instanceDefaultDarkTheme: "เฆ‡เฆจเงเฆธเฆŸเงเฆฏเฆพเฆจเงเฆธเง‡เฆฐ เฆกเฆฟเฆซเฆฒเงเฆŸ เฆกเฆพเฆฐเงเฆ• เฆฅเฆฟเฆฎ" +instanceDefaultThemeDescription: "เฆ…เฆฌเฆœเง‡เฆ•เงเฆŸ เฆซเฆฐเฆฎเงเฆฏเฆพเฆŸเง‡ เฆฅเฆฟเฆฎ เฆ•เง‹เฆก เฆฒเฆฟเฆ–เงเฆจ" +mutePeriod: "เฆฎเฆฟเฆ‰เฆŸเง‡เฆฐ เฆธเฆฎเงŸเฆ•เฆพเฆฒ" indefinitely: "เฆ…เฆจเฆฟเฆฐเงเฆฆเฆฟเฆทเงเฆŸ" tenMinutes: "เงงเงฆ เฆฎเฆฟเฆจเฆฟเฆŸ" oneHour: "เงง เฆ˜เฆฃเงเฆŸเฆพ" oneDay: "เฆเฆ•เฆฆเฆฟเฆจ" oneWeek: "เฆเฆ• เฆธเฆชเงเฆคเฆพเฆน" +reflectMayTakeTime: "เฆเฆŸเฆฟเฆฐ เฆ•เฆพเฆœ เฆฆเง‡เฆ–เฆพ เฆฏเง‡เฆคเง‡ เฆ•เฆฟเฆ›เงเฆŸเฆพ เฆธเฆฎเฆฏเฆผ เฆฒเฆพเฆ—เฆคเง‡ เฆชเฆพเฆฐเง‡เฅค" +failedToFetchAccountInformation: "เฆ…เงเฆฏเฆพเฆ•เฆพเฆ‰เฆจเงเฆŸเง‡เฆฐ เฆคเฆฅเงเฆฏ เฆ‰เฆฆเงเฆงเฆพเฆฐ เฆ•เฆฐเฆพ เฆฏเฆพเงŸเฆจเฆฟ" +rateLimitExceeded: "เฆฐเง‡เฆŸ เฆฒเฆฟเฆฎเฆฟเฆŸ เฆ›เฆพเงœเฆฟเงŸเง‡ เฆ—เง‡เฆ›เง‡ " _emailUnavailable: used: "เฆเฆ‡ เฆ‡เฆฎเง‡เฆ‡เฆฒ เฆ เฆฟเฆ•เฆพเฆจเฆพเฆŸเฆฟ เฆ‡เฆคเง‹เฆฎเฆงเงเฆฏเง‡ เฆฌเงเฆฏเฆฌเฆนเงƒเฆค เฆนเงŸเง‡เฆ›เง‡" format: "เฆเฆ‡ เฆ‡เฆฎเง‡เฆฒ เฆ เฆฟเฆ•เฆพเฆจเฆพเฆŸเฆฟ เฆธเฆ เฆฟเฆ•เฆญเฆพเฆฌเง‡ เฆฒเฆฟเฆ–เฆพ เฆนเงŸเฆจเฆฟ" @@ -1081,7 +1088,6 @@ _sfx: antenna: "เฆ…เงเฆฏเฆพเฆจเงเฆŸเง‡เฆจเฆพเฆ—เงเฆฒเฆฟ" channel: "เฆšเงเฆฏเฆพเฆจเง‡เฆฒเง‡เฆฐ เฆฌเฆฟเฆœเงเฆžเฆชเงเฆคเฆฟ" _ago: - unknown: "เฆ…เฆœเฆพเฆจเฆพ" future: "เฆญเฆฌเฆฟเฆทเงเฆฏเงŽ" justNow: "เฆเฆ‡เฆฎเฆพเฆคเงเฆฐ" secondsAgo: "{n} เฆธเง‡เฆ•เง‡เฆจเงเฆก เฆ†เฆ—เง‡" @@ -1125,6 +1131,7 @@ _2fa: registerKey: "เฆธเฆฟเฆ•เฆฟเฆ‰เฆฐเฆฟเฆŸเฆฟ เฆ•เง€ เฆจเฆฟเฆฌเฆจเงเฆงเฆจ เฆ•เฆฐเงเฆจ" step1: "เฆชเงเฆฐเฆฅเฆฎเง‡, เฆ†เฆชเฆจเฆพเฆฐ เฆกเฆฟเฆญเฆพเฆ‡เฆธเง‡ {a} เฆฌเฆพ {b} เฆเฆฐ เฆฎเฆคเง‹ เฆเฆ•เฆŸเฆฟ เฆ…เฆฅเง‡เฆจเฆŸเฆฟเฆ•เง‡เฆถเฆจ เฆ…เงเฆฏเฆพเฆช เฆ‡เฆจเฆธเงเฆŸเฆฒ เฆ•เฆฐเงเฆจเงท" step2: "เฆเฆฐเฆชเฆฐเง‡, เฆ…เงเฆฏเฆพเฆชเง‡เฆฐ เฆธเฆพเฆนเฆพเฆฏเงเฆฏเง‡ เฆชเงเฆฐเฆฆเฆฐเงเฆถเฆฟเฆค QR เฆ•เง‹เฆกเฆŸเฆฟ เฆธเงเฆ•เงเฆฏเฆพเฆจ เฆ•เฆฐเงเฆจเฅค" + step2Url: "เฆกเง‡เฆธเงเฆ•เฆŸเฆช เฆ…เงเฆฏเฆพเฆชเง‡, เฆจเฆฟเฆฎเงเฆจเฆฒเฆฟเฆ–เฆฟเฆค URL เฆฒเฆฟเฆ–เงเฆจ:" step3: "เฆ…เงเฆฏเฆพเฆชเง‡ เฆชเงเฆฐเฆฆเฆฐเงเฆถเฆฟเฆค เฆŸเง‹เฆ•เง‡เฆจเฆŸเฆฟ เฆฒเฆฟเฆ–เงเฆจ เฆเฆฌเฆ‚ เฆ†เฆชเฆจเฆพเฆฐ เฆ•เฆพเฆœ เฆถเง‡เฆทเฅค" step4: "เฆ†เฆชเฆจเฆพเฆ•เง‡ เฆเฆ–เฆจ เฆฅเง‡เฆ•เง‡ เฆฒเฆ— เฆ‡เฆจ เฆ•เฆฐเฆพเฆฐ เฆธเฆฎเฆฏเฆผ, เฆเฆ‡เฆญเฆพเฆฌเง‡ เฆŸเง‹เฆ•เง‡เฆจ เฆฒเฆฟเฆ–เฆคเง‡ เฆนเฆฌเง‡เฅค" securityKeyInfo: "เฆ†เฆชเฆจเฆฟ เฆเฆ•เฆŸเฆฟ เฆนเฆพเฆฐเงเฆกเฆ“เฆฏเฆผเงเฆฏเฆพเฆฐ เฆธเฆฟเฆ•เฆฟเฆ‰เฆฐเฆฟเฆŸเฆฟ เฆ•เง€ เฆฌเงเฆฏเฆฌเฆนเฆพเฆฐ เฆ•เฆฐเง‡ เฆฒเฆ— เฆ‡เฆจ เฆ•เฆฐเฆคเง‡ เฆชเฆพเฆฐเง‡เฆจ เฆฏเฆพ FIDO2 เฆฌเฆพ เฆกเฆฟเฆญเฆพเฆ‡เฆธเง‡เฆฐ เฆซเฆฟเฆ™เงเฆ—เฆพเฆฐเฆชเงเฆฐเฆฟเฆจเงเฆŸ เฆธเง‡เฆจเงเฆธเฆฐ เฆฌเฆพ เฆชเฆฟเฆจ เฆธเฆฎเฆฐเงเฆฅเฆจ เฆ•เฆฐเง‡เงท" @@ -1608,6 +1615,8 @@ _notification: youReceivedFollowRequest: "เฆ…เฆจเงเฆธเฆฐเฆฃ เฆ•เฆฐเฆพเฆฐ เฆœเฆจเงเฆฏ เฆ…เฆจเงเฆฐเง‹เฆง เฆชเฆพเฆ“เงŸเฆพ เฆ—เง‡เฆ›เง‡" yourFollowRequestAccepted: "เฆ†เฆชเฆจเฆพเฆฐ เฆ…เฆจเงเฆธเฆฐเฆฃ เฆ•เฆฐเฆพเฆฐ เฆ…เฆจเงเฆฐเง‹เฆง เฆ—เงƒเฆนเง€เฆค เฆนเงŸเง‡เฆ›เง‡" youWereInvitedToGroup: "เฆ†เฆชเฆจเฆฟ เฆเฆ•เฆŸเฆฟ เฆ—เงเฆฐเงเฆชเง‡ เฆ†เฆฎเฆจเงเฆคเงเฆฐเฆฟเฆค เฆนเงŸเง‡เฆ›เง‡เฆจ" + pollEnded: "เฆชเง‹เฆฒเง‡เฆฐ เฆซเฆฒเฆพเฆซเฆฒ เฆฆเง‡เฆ–เฆพ เฆฏเฆพเฆฌเง‡" + emptyPushNotificationMessage: "เฆ†เฆชเฆกเง‡เฆŸ เฆ•เฆฐเฆพ เฆชเงเฆถ เฆฌเฆฟเฆœเงเฆžเฆชเงเฆคเฆฟ" _types: all: "เฆธเฆ•เฆฒ" follow: "เฆ…เฆจเงเฆธเฆฐเฆฃ เฆ•เฆฐเฆพ เฆนเฆšเงเฆ›เง‡" @@ -1617,10 +1626,15 @@ _notification: quote: "เฆ‰เฆฆเงเฆงเงƒเฆคเฆฟ" reaction: "เฆชเงเฆฐเฆคเฆฟเฆ•เงเฆฐเฆฟเฆฏเฆผเฆพ" pollVote: "เฆชเง‹เฆฒเง‡ เฆญเง‹เฆŸ เฆ†เฆ›เง‡" + pollEnded: "เฆชเง‹เฆฒ เฆถเง‡เฆท" receiveFollowRequest: "เฆชเงเฆฐเฆพเฆชเงเฆค เฆ…เฆจเงเฆธเฆฐเฆฃเง‡เฆฐ เฆ…เฆจเงเฆฐเง‹เฆงเฆธเฆฎเง‚เฆน" followRequestAccepted: "เฆ—เงƒเฆนเง€เฆค เฆ…เฆจเงเฆธเฆฐเฆฃเง‡เฆฐ เฆ…เฆจเงเฆฐเง‹เฆงเฆธเฆฎเง‚เฆน" groupInvited: "เฆ—เงเฆฐเงเฆชเง‡เฆฐ เฆ†เฆฎเฆจเงเฆคเงเฆฐเฆจเฆธเฆฎเง‚เฆน" app: "เฆฒเฆฟเฆ™เงเฆ• เฆ•เฆฐเฆพ เฆ…เงเฆฏเฆพเฆช เฆฅเง‡เฆ•เง‡ เฆฌเฆฟเฆœเงเฆžเฆชเงเฆคเฆฟ" + _actions: + followBack: "เฆซเฆฒเง‹ เฆฌเงเฆฏเฆพเฆ• เฆ•เฆฐเง‡เฆ›เง‡" + reply: "เฆœเฆฌเฆพเฆฌ" + renote: "เฆฐเฆฟเฆจเง‹เฆŸ" _deck: alwaysShowMainColumn: "เฆธเฆฐเงเฆฌเฆฆเฆพ เฆฎเง‡เฆ‡เฆจ เฆ•เฆฒเฆพเฆฎ เฆฆเง‡เฆ–เฆพเฆจ" columnAlign: "เฆ•เฆฒเฆพเฆฎ เฆธเฆพเฆœเฆพเฆจ" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 5f74cb6bef..74eab3603b 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1,6 +1,8 @@ --- _lang_: "Catalร " headlineMisskey: "Una xarxa connectada per notes" +introMisskey: "Benvingut! Misskey รฉs un servei de microblogging descentralitzat de codi obert.\nCrea \"notes\" per compartir els teus pensaments amb tots els que t'envolten. ๐Ÿ“ก\nAmb \"reaccions\", tambรฉ pots expressar rร pidament els teus sentiments sobre les notes de tothom. ๐Ÿ‘\nExplorem un mรณn nou! ๐Ÿš€" +monthAndDay: "{day}/{month}" search: "Cercar" notifications: "Notificacions" username: "Nom d'usuari" @@ -10,17 +12,175 @@ fetchingAsApObject: "Cercant en el Fediverse..." ok: "OK" gotIt: "Ho he entรจs!" cancel: "Cancelยทlar" +enterUsername: "Introdueix el teu nom d'usuari" +renotedBy: "Resignat per {usuari}" +noNotes: "Cap nota" +noNotifications: "Cap notificaciรณ" +instance: "Instร ncies" +settings: "Preferรจncies" +basicSettings: "Configuraciรณ bร sica" +otherSettings: "Configuraciรณ avanรงada" +openInWindow: "Obrir en una nova finestra" +profile: "Perfil" +timeline: "Lรญnia de temps" +noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia." +login: "Iniciar sessiรณ" +loggingIn: "Identificant-se" +logout: "Tancar la sessiรณ" +signup: "Registrar-se" +uploading: "Pujant..." +save: "Desar" +users: "Usuaris" +addUser: "Afegir un usuari" +favorite: "Afegir a preferits" +favorites: "Favorits" +unfavorite: "Eliminar dels preferits" +favorited: "Afegit als preferits." +alreadyFavorited: "Ja s'ha afegit als preferits." +cantFavorite: "No s'ha pogut afegir als preferits." +pin: "Fixar al perfil" +unpin: "Para de fixar del perfil" +copyContent: "Copiar el contingut" +copyLink: "Copiar l'enllaรง" +delete: "Eliminar" +deleteAndEdit: "Esborrar i editar" +deleteAndEditConfirm: "Estร s segur que vols suprimir aquesta nota i editar-la? Perdrร s totes les reaccions, notes i respostes." +addToList: "Afegir a una llista" +sendMessage: "Enviar un missatge" +copyUsername: "Copiar nom d'usuari" +searchUser: "Cercar usuaris" +reply: "Respondre" +loadMore: "Carregar mรฉs" +showMore: "Veure mรฉs" +youGotNewFollower: "t'ha seguit" +receiveFollowRequest: "Solยทlicitud de seguiment rebuda" +followRequestAccepted: "Solยทlicitud de seguiment acceptada" +mention: "Menciรณ" +mentions: "Mencions" +directNotes: "Notes directes" +importAndExport: "Importar / Exportar" +import: "Importar" +export: "Exportar" +files: "Fitxers" +download: "Baixar" +driveFileDeleteConfirm: "Estร s segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer adjunt tambรฉ se suprimiran." +unfollowConfirm: "Estร s segur que vols deixar de seguir {name}?" +exportRequested: "Has solยทlicitat una exportaciรณ. Aixรฒ pot trigar una estona. S'afegirร  a la teva unitat un cop completat." +importRequested: "Has solยทlicitat una importaciรณ. Aixรฒ pot trigar una estona." +lists: "Llistes" +noLists: "No tens cap llista" +note: "Nota" +notes: "Notes" +following: "Seguint" +followers: "Seguidors" +followsYou: "Et segueix" +createList: "Crear llista" +manageLists: "Gestionar les llistes" +error: "Error" +somethingHappened: "S'ha produรฏt un error" +retry: "Torna-ho a intentar" +pageLoadError: "S'ha produรฏt un error en carregar la pร gina" +pageLoadErrorDescription: "Aixรฒ normalment es deu a errors de xarxa o a la memรฒria cau del navegador. Prova d'esborrar la memรฒria cau i torna-ho a provar desprรฉs d'esperar una estona." +serverIsDead: "Aquest servidor no respon. Espera una estona i torna-ho a provar." +youShouldUpgradeClient: "Per veure aquesta pร gina, actualitzeu-la per actualitzar el vostre client." +enterListName: "Introdueix un nom per a la llista" +privacy: "Privadesa" +makeFollowManuallyApprove: "Les solยทlicituds de seguiment requereixen aprovaciรณ" +defaultNoteVisibility: "Visibilitat per defecte" +follow: "Seguint" +followRequest: "Enviar la solยทlicitud de seguiment" +followRequests: "Solยทlicituds de seguiment" +unfollow: "Deixar de seguir" +followRequestPending: "Solยทlicituds de seguiment pendents" +enterEmoji: "Introduir un emoji" +renote: "Renotar" +unrenote: "Anulยทlar renota" +renoted: "Renotat." +cantRenote: "Aquesta publicaciรณ no pot ser renotada." +cantReRenote: "Impossible renotar una renota." +quote: "Citar" +pinnedNote: "Nota fixada" +pinned: "Fixar al perfil" +you: "Tu" +clickToShow: "Fes clic per mostrar" +sensitive: "NSFW" +add: "Afegir" +reaction: "Reaccions" +reactionSetting: "Reaccions a mostrar al selector de reaccions" +reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir." +rememberNoteVisibility: "Recorda la configuraciรณ de visibilitat de les notes" +attachCancel: "Eliminar el fitxer adjunt" +markAsSensitive: "Marcar com a NSFW" +instances: "Instร ncies" +remove: "Eliminar" +nsfw: "NSFW" +pinnedNotes: "Nota fixada" +userList: "Llistes" smtpUser: "Nom d'usuari" smtpPass: "Contrasenya" +user: "Usuaris" searchByGoogle: "Cercar" +_email: + _follow: + title: "t'ha seguit" _mfm: + mention: "Menciรณ" + quote: "Citar" search: "Cercar" +_theme: + keys: + mention: "Menciรณ" + renote: "Renotar" _sfx: + note: "Notes" notification: "Notificacions" +_2fa: + step2Url: "Tambรฉ pots inserir aquest enllaรง i utilitzes una aplicaciรณ d'escriptori:" _widgets: notifications: "Notificacions" + timeline: "Lรญnia de temps" +_cw: + show: "Carregar mรฉs" +_visibility: + followers: "Seguidors" _profile: username: "Nom d'usuari" +_exportOrImport: + followingList: "Seguint" + userLists: "Llistes" +_pages: + script: + categories: + list: "Llistes" + blocks: + _join: + arg1: "Llistes" + _randomPick: + arg1: "Llistes" + _dailyRandomPick: + arg1: "Llistes" + _seedRandomPick: + arg2: "Llistes" + _pick: + arg1: "Llistes" + _listLen: + arg1: "Llistes" + types: + array: "Llistes" +_notification: + youWereFollowed: "t'ha seguit" + _types: + follow: "Seguint" + mention: "Menciรณ" + renote: "Renotar" + quote: "Citar" + reaction: "Reaccions" + _actions: + reply: "Respondre" + renote: "Renotar" _deck: _columns: notifications: "Notificacions" + tl: "Lรญnia de temps" + list: "Llistes" + mentions: "Mencions" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 2f5e375372..4b20340df1 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -53,6 +53,8 @@ reply: "Odpovฤ›dฤ›t" loadMore: "Zobrazit vรญce" showMore: "Zobrazit vรญce" youGotNewFollower: "Mรกte novรฉho nรกsledovnรญka" +receiveFollowRequest: "ลฝรกdost o sledovรกnรญ pล™ijata" +followRequestAccepted: "ลฝรกdost o sledovรกnรญ pล™ijata" mention: "Zmรญnฤ›nรญ" mentions: "Zmรญnฤ›nรญ" importAndExport: "Import a export" @@ -60,7 +62,9 @@ import: "Importovat" export: "Exportovat" files: "Soubor(ลฏ)" download: "Stรกhnout" +driveFileDeleteConfirm: "Opravdu chcete smazat soubor \"{name}\"? Poznรกmky, ke kterรฝm je tento soubor pล™ipojen, budou takรฉ smazรกny." unfollowConfirm: "Jste si jisti ลพe uลพ nechcete sledovat {name}?" +exportRequested: "Poลพรกdali jste o export. To mลฏลพe chvรญli trvat. Pล™idรกme ho na vรกลก Disk aลพ bude dokonฤen." importRequested: "Poลพรกdali jste o export. To mลฏลพe chvilku trvat." lists: "Seznamy" noLists: "Nemรกte ลพรกdnรฉ seznamy" @@ -75,13 +79,25 @@ error: "Chyba" somethingHappened: "Jejda. Nฤ›co se nepovedlo." retry: "Opakovat" pageLoadError: "Nepodaล™ilo se naฤรญst strรกnku" +serverIsDead: "Server neodpovรญdรก. Poฤkejte chvรญli a zkuste to znovu." +youShouldUpgradeClient: "Pro zobrazenรญ tรฉto strรกnky obnovte strรกnku pro aktualizaci klienta." enterListName: "Jmรฉno seznamu" privacy: "Soukromรญ" +makeFollowManuallyApprove: "ลฝรกdosti o sledovรกnรญ vyลพadujรญ potvrzenรญ" +defaultNoteVisibility: "Vรฝchozรญ viditelnost" follow: "Sledovanรญ" +followRequest: "Odeslat ลพรกdost o sledovรกnรญ" +followRequests: "ลฝรกdosti o sledovรกnรญ" unfollow: "Pล™estat sledovat" +followRequestPending: "ฤŒekajรญcรญ ลพรกdosti o sledovรกnรญ" +enterEmoji: "Vloลพte emoji" renote: "Pล™eposlat" +unrenote: "Zruลกit pล™eposlรกnรญ" +renoted: "Pล™eposlรกno" +cantRenote: "Tento pล™รญspฤ›vek nelze pล™eposlat." cantReRenote: "Odpovฤ›ฤ nemลฏลพe bรฝt odstranฤ›na." quote: "Citovat" +pinnedNote: "Pล™ipnutรก poznรกmka" pinned: "Pล™ipnout" you: "Vy" clickToShow: "Kliknฤ›te pro zobrazenรญ" @@ -122,6 +138,8 @@ flagAsBot: "Tento รบฤet je bot" flagAsBotDescription: "Pokud je tento รบฤet kontrolovรกn programem zaลกkrtnฤ›te tuto moลพnost. To oznaฤรญ tento รบฤet jako bot pro ostatnรญ vรฝvojรกล™e a zabrรกnรญ tak nekoneฤnรฝm interakcรญm s ostatnรญmi boty a upravรญ Misskey systรฉm aby se choval k tomuhle รบฤtu jako bot." flagAsCat: "Tenhle รบฤet je koฤka" flagAsCatDescription: "Vyberte tuto moลพnost aby tento รบฤet byl oznaฤen jako koฤka." +flagShowTimelineReplies: "Zobrazovat odpovฤ›di na ฤasovรฉ ose" +flagShowTimelineRepliesDescription: "Je-li zapnuto, zobrazรญ odpovฤ›di uลพivatelลฏ na poznรกmky jinรฝch uลพivatelลฏ na vaลกรญ ฤasovรฉ ose." autoAcceptFollowed: "Automaticky akceptovat nรกsledovรกnรญ od รบฤtลฏ kterรฉ sledujete" addAccount: "Pล™idat รบฤet" loginFailed: "Pล™ihlรกลกenรญ se nezdaล™ilo." @@ -130,13 +148,16 @@ general: "Obecnฤ›" wallpaper: "Obrรกzek na pozadรญ" setWallpaper: "Nastavenรญ obrรกzku na pozadรญ" removeWallpaper: "Odstranit pozadรญ" +searchWith: "Hledat: {q}" youHaveNoLists: "Nemรกte ลพรกdnรฉ seznamy" +followConfirm: "Jste si jisti, ลพe chcete sledovat {name}?" proxyAccount: "Proxy รบฤet" proxyAccountDescription: "Proxy รบฤet je รบฤet, kterรฝ za urฤitรฝch podmรญnek sleduje uลพivatele na dรกlku vaลกรญm jmรฉnem. Napล™รญklad kdyลพ uลพivatel zaล™adรญ vzdรกlenรฉho uลพivatele do seznamu, pokud nikdo nesleduje uลพivatele na seznamu, aktivita nebude doruฤena instanci, takลพe mรญsto toho bude uลพivatele sledovat รบฤet proxy." host: "Hostitel" selectUser: "Vyberte uลพivatele" recipient: "Pro" annotation: "Komentรกล™e" +federation: "Federace" instances: "Instance" registeredAt: "Registrovรกn" latestRequestSentAt: "Poslednรญ poลพadavek poslรกn" @@ -146,6 +167,7 @@ storageUsage: "Vyuลพitรญ รบloลพiลกtฤ›" charts: "Grafy" perHour: "za hodinu" perDay: "za den" +stopActivityDelivery: "Pล™estat zasรญlat aktivitu" blockThisInstance: "Blokovat tuto instanci" operations: "Operace" software: "Software" @@ -283,6 +305,8 @@ iconUrl: "Favicon URL" bannerUrl: "Baner URL" backgroundImageUrl: "Adresa URL obrรกzku pozadรญ" basicInfo: "Zรกkladnรญ informace" +pinnedUsers: "Pล™ipnutรญ uลพivatelรฉ" +pinnedNotes: "Pล™ipnutรก poznรกmka" hcaptcha: "hCaptcha" enableHcaptcha: "Aktivovat hCaptchu" hcaptchaSecretKey: "Tajnรฝ Klรญฤ (Secret Key)" @@ -471,6 +495,7 @@ _widgets: notifications: "Oznรกmenรญ" timeline: "ฤŒasovรก osa" activity: "Aktivita" + federation: "Federace" jobQueue: "Fronta รบloh" _cw: show: "Zobrazit vรญce" @@ -485,6 +510,8 @@ _exportOrImport: muteList: "Ztlumit" blockingList: "Zablokovat" userLists: "Seznamy" +_charts: + federation: "Federace" _timelines: home: "Domลฏ" _pages: @@ -517,6 +544,9 @@ _notification: renote: "Pล™eposlat" quote: "Citovat" reaction: "Reakce" + _actions: + reply: "Odpovฤ›dฤ›t" + renote: "Pล™eposlat" _deck: _columns: notifications: "Oznรกmenรญ" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 1f558787ab..5dfce28002 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -842,6 +842,9 @@ oneDay: "Einen Tag" oneWeek: "Eine Woche" reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt." failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden" +rateLimitExceeded: "Versuchsanzahl รผberschritten" +cropImage: "Bild zuschneiden" +cropImageAsk: "Mรถchtest du das Bild zuschneiden?" _emailUnavailable: used: "Diese Email-Adresse wird bereits verwendet" format: "Das Format dieser Email-Adresse ist ungรผltig" @@ -1006,7 +1009,7 @@ _instanceMute: heading: "Liste der stummzuschaltenden Instanzen" _theme: explore: "Farbschemata erforschen" - install: "Farbschmata installieren" + install: "Farbschemata installieren" manage: "Farbschemaverwaltung" code: "Farbschemencode" description: "Beschreibung" @@ -1087,7 +1090,6 @@ _sfx: antenna: "Antennen" channel: "Kanalbenachrichtigung" _ago: - unknown: "Unbekannt" future: "Zukunft" justNow: "Gerade eben" secondsAgo: "vor {n} Sekunde(n)" @@ -1131,6 +1133,7 @@ _2fa: registerKey: "Neuen Sicherheitsschlรผssel registrieren" step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerรคt." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerรคt." + step2Url: "Nutzt du ein Desktopprogramm kannst du alternativ diese URL eingeben:" step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird." step4: "Alle folgenden Anmeldungsversuche werden ab sofort die Eingabe eines solchen Tokens benรถtigen." securityKeyInfo: "Du kannst neben Fingerabdruck- oder PIN-Authentifizierung auf deinem Gerรคt auch Anmeldung mit Hilfe eines FIDO2-kompatiblen Hardware-Sicherheitsschlรผssels einrichten." @@ -1613,8 +1616,9 @@ _notification: youWereFollowed: "ist dir gefolgt" youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" - youWereInvitedToGroup: "Du wurdest in eine Gruppe eingeladen" + youWereInvitedToGroup: "{userName} hat dich in eine Gruppe eingeladen" pollEnded: "Umfrageergebnisse sind verfรผgbar" + emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert" _types: all: "Alle" follow: "Neue Follower" @@ -1629,6 +1633,10 @@ _notification: followRequestAccepted: "Akzeptierte Follow-Anfragen" groupInvited: "Erhaltene Gruppeneinladungen" app: "Benachrichtigungen von Apps" + _actions: + followBack: "folgt dir nun auch" + reply: "Antworten" + renote: "Renote" _deck: alwaysShowMainColumn: "Hauptspalte immer zeigen" columnAlign: "Spaltenausrichtung" diff --git a/locales/en-US.yml b/locales/en-US.yml index 99fe05375b..8bfea26b0f 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -842,6 +842,9 @@ oneDay: "One day" oneWeek: "One week" reflectMayTakeTime: "It may take some time for this to be reflected." failedToFetchAccountInformation: "Could not fetch account information" +rateLimitExceeded: "Rate limit exceeded" +cropImage: "Crop image" +cropImageAsk: "Do you want to crop this image?" _emailUnavailable: used: "This email address is already being used" format: "The format of this email address is invalid" @@ -1087,7 +1090,6 @@ _sfx: antenna: "Antennas" channel: "Channel notifications" _ago: - unknown: "Unknown" future: "Future" justNow: "Just now" secondsAgo: "{n} second(s) ago" @@ -1131,6 +1133,7 @@ _2fa: registerKey: "Register a security key" step1: "First, install an authentication app (such as {a} or {b}) on your device." step2: "Then, scan the QR code displayed on this screen." + step2Url: "You can also enter this URL if you're using a desktop program:" step3: "Enter the token provided by your app to finish setup." step4: "From now on, any future login attempts will ask for such a login token." securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure your account." @@ -1613,8 +1616,9 @@ _notification: youWereFollowed: "followed you" youReceivedFollowRequest: "You've received a follow request" yourFollowRequestAccepted: "Your follow request was accepted" - youWereInvitedToGroup: "You've been invited to a group" + youWereInvitedToGroup: "{userName} invited you to a group" pollEnded: "Poll results have become available" + emptyPushNotificationMessage: "Push notifications have been updated" _types: all: "All" follow: "New followers" @@ -1629,6 +1633,10 @@ _notification: followRequestAccepted: "Accepted follow requests" groupInvited: "Group invitations" app: "Notifications from linked apps" + _actions: + followBack: "followed you back" + reply: "Reply" + renote: "Renote" _deck: alwaysShowMainColumn: "Always show main column" columnAlign: "Align columns" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index fd69f62ff5..6c10942b48 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -141,6 +141,8 @@ flagAsBot: "Esta cuenta es un bot" flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opciรณn. Al hacerlo, esta opciรณn servirรก para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustarรก los sistemas internos de Misskey para que trate a esta cuenta como un bot." flagAsCat: "Esta cuenta es un gato" flagAsCatDescription: "En caso de que declare que esta cuenta es de un gato, active esta opciรณn." +flagShowTimelineReplies: "Mostrar respuestas a las notas en la biografรญa" +flagShowTimelineRepliesDescription: "Cuando se marca, la lรญnea de tiempo muestra respuestas a otras notas ademรกs de las notas del usuario" autoAcceptFollowed: "Aceptar automรกticamente las solicitudes de seguimiento de los usuarios que sigues" addAccount: "Agregar Cuenta" loginFailed: "Error al iniciar sesiรณn." @@ -235,6 +237,8 @@ resetAreYouSure: "ยฟDesea reestablecer?" saved: "Guardado" messaging: "Chat" upload: "Subir" +keepOriginalUploading: "Mantener la imagen original" +keepOriginalUploadingDescription: "Mantener la versiรณn original al cargar imรกgenes. Si estรก desactivado, el navegador generarรก imรกgenes para la publicaciรณn web en el momento de recargar la pรกgina" fromDrive: "Desde el drive" fromUrl: "Desde la URL" uploadFromUrl: "Subir desde una URL" @@ -444,6 +448,7 @@ uiLanguage: "Idioma de visualizaciรณn de la interfaz" groupInvited: "Invitado al grupo" aboutX: "Acerca de {x}" useOsNativeEmojis: "Usa los emojis nativos de la plataforma" +disableDrawer: "No mostrar los menรบs en cajones" youHaveNoGroups: "Sin grupos" joinOrCreateGroup: "Obtenga una invitaciรณn para unirse al grupos o puede crear su propio grupo." noHistory: "No hay datos en el historial" @@ -615,6 +620,10 @@ reportAbuse: "Reportar" reportAbuseOf: "Reportar a {name}" fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en particular, ingrese la URL de esta." abuseReported: "Se ha enviado el reporte. Muchas gracias." +reporteeOrigin: "Informar a" +reporterOrigin: "Origen del informe" +forwardReport: "Transferir un informe a una instancia remota" +forwardReportIsAnonymous: "No puede ver su informaciรณn de la instancia remota y aparecerรก como una cuenta anรณnima del sistema" send: "Enviar" abuseMarkAsResolved: "Marcar reporte como resuelto" openInNewTab: "Abrir en una Nueva Pestaรฑa" @@ -676,6 +685,7 @@ center: "Centrar" wide: "Ancho" narrow: "Estrecho" reloadToApplySetting: "Esta configuraciรณn sรณlo se aplicarรก despuรฉs de recargar la pรกgina. ยฟRecargar ahora?" +needReloadToApply: "Se requiere un reinicio para la aplicar los cambios" showTitlebar: "Mostrar la barra de tรญtulo" clearCache: "Limpiar cachรฉ" onlineUsersCount: "{n} usuarios en lรญnea" @@ -706,19 +716,55 @@ capacity: "Capacidad" inUse: "Usado" editCode: "Editar cรณdigo" apply: "Aplicar" +receiveAnnouncementFromInstance: "Recibir notificaciones de la instancia" +emailNotification: "Notificaciones por correo electrรณnico" publish: "Publicar" inChannelSearch: "Buscar en el canal" +useReactionPickerForContextMenu: "Haga clic con el botรณn derecho para abrir el menu de reacciones" +typingUsers: "{users} estรก escribiendo" +jumpToSpecifiedDate: "Saltar a una fecha especรญfica" +showingPastTimeline: "Mostrar lรญneas de tiempo antiguas" +clear: "Limpiar" markAllAsRead: "Marcar todo como leรญdo" goBack: "Deseleccionar" +fullView: "Vista completa" +quitFullView: "quitar vista completa" +addDescription: "Agregar descripciรณn" +userPagePinTip: "Puede mantener sus notas visibles aquรญ seleccionando Pin en el menรบ de notas individuales" +notSpecifiedMentionWarning: "Algunas menciones no estรกn incluidas en el destino" info: "Informaciรณn" +userInfo: "Informaciรณn del usuario" +unknown: "Desconocido" +onlineStatus: "En lรญnea" +hideOnlineStatus: "mostrarse como desconectado" +hideOnlineStatusDescription: "Ocultar su estado en lรญnea puede reducir la eficacia de algunas funciones, como la bรบsqueda" online: "En lรญnea" +active: "Activo" offline: "Sin conexiรณn" +notRecommended: "obsoleto" +botProtection: "Protecciรณn contra bots" +instanceBlocking: "Instancias bloqueadas" +selectAccount: "Elija una cuenta" +switchAccount: "Cambiar de cuenta" +enabled: "Activado" +disabled: "Desactivado" +quickAction: "Acciones rรกpidas" user: "Usuarios" administration: "Administrar" +accounts: "Cuentas" +switch: "Cambiar" +noMaintainerInformationWarning: "No se ha establecido la informaciรณn del administrador" +noBotProtectionWarning: "La protecciรณn contra los bots no estรก configurada" +configure: "Configurar" +postToGallery: "Crear una nueva publicaciรณn en la galerรญa" gallery: "Galerรญa" recentPosts: "Posts recientes" popularPosts: "Mรกs vistos" +shareWithNote: "Compartir con una nota" +ads: "Anuncios" expiration: "Termina el" +memo: "Notas" +priority: "Prioridad" high: "Alta" middle: "Mediano" low: "Baja" @@ -770,22 +816,50 @@ _accountDelete: accountDelete: "Eliminar Cuenta" _ad: back: "Deseleccionar" +_forgotPassword: + contactAdmin: "Esta instancia no admite el uso de direcciones de correo electrรณnico, pรณngase en contacto con el administrador de la instancia para restablecer su contraseรฑa" _gallery: my: "Mi galerรญa" + liked: "Publicaciones que me gustan" + like: "ยกMuy bien!" unlike: "Quitar me gusta" _email: _follow: title: "te ha seguido" + _receiveFollowRequest: + title: "Has recibido una solicitud de seguimiento" +_plugin: + install: "Instalar plugins" + installWarn: "Por favor no instale plugins que no son de confianza" + manage: "Gestionar plugins" _registry: + scope: "Alcance" key: "Clave" keys: "Clave" + domain: "Dominio" + createKey: "Crear una llave" +_aboutMisskey: + about: "Misskey es un software de cรณdigo abierto, desarrollado por syuilo desde el 2014" + contributors: "Principales colaboradores" + allContributors: "Todos los colaboradores" + source: "Cรณdigo fuente" + translation: "Traducir Misskey" + donate: "Donar a Misskey" + morePatrons: "Muchas mรกs personas nos apoyan. Muchas gracias๐Ÿฅฐ" + patrons: "Patrocinadores" +_nsfw: + respect: "Ocultar medios NSFW" + ignore: "No esconder medios NSFW " + force: "Ocultar todos los medios" _mfm: cheatSheet: "Hoja de referencia de MFM" intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de Misskey. Aquรญ puede ver una lista de sintaxis disponibles en MFM." + dummy: "Misskey expande el mundo de la Fediverso" mention: "Menciones" mentionDescription: "El signo @ seguido de un nombre de usuario se puede utilizar para notificar a un usuario en particular." hashtag: "Hashtag" url: "URL" + urlDescription: "Se pueden mostrar las URL" link: "Vรญnculo" bold: "Negrita" center: "Centrar" @@ -915,7 +989,6 @@ _sfx: antenna: "Antena receptora" channel: "Notificaciones del canal" _ago: - unknown: "Desconocido" future: "Futuro" justNow: "Reciรฉn ahora" secondsAgo: "Hace {n} segundos" @@ -1432,6 +1505,9 @@ _notification: followRequestAccepted: "El seguimiento fue aceptado" groupInvited: "Invitado al grupo" app: "Notificaciones desde aplicaciones" + _actions: + reply: "Responder" + renote: "Renotar" _deck: alwaysShowMainColumn: "Siempre mostrar la columna principal" columnAlign: "Alinear columnas" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 1fe74fa9ab..7e225c2992 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -804,7 +804,7 @@ manageAccounts: "Gรฉrer les comptes" makeReactionsPublic: "Rendre les rรฉactions publiques" makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos rรฉactions donnรฉes publique." classic: "Classique" -muteThread: "Mettre ce thread en sourdine" +muteThread: "Masquer cette discussion" unmuteThread: "Ne plus masquer le fil" ffVisibility: "Visibilitรฉ des abonnรฉs/abonnements" ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent." @@ -1075,7 +1075,6 @@ _sfx: antenna: "Rรฉception de lโ€™antenne" channel: "Notifications de canal" _ago: - unknown: "Inconnu" future: "Futur" justNow: "ร  lโ€™instant" secondsAgo: "Il y a {n}s" @@ -1615,6 +1614,9 @@ _notification: followRequestAccepted: "Demande d'abonnement acceptรฉe" groupInvited: "Invitation ร  un groupe" app: "Notifications provenant des apps" + _actions: + reply: "Rรฉpondre" + renote: "Renoter" _deck: alwaysShowMainColumn: "Toujours afficher la colonne principale" columnAlign: "Aligner les colonnes" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 11dff184cd..39e2c1f661 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -593,6 +593,7 @@ smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS" testEmail: "Tes pengiriman surel" wordMute: "Bisukan kata" regexpError: "Kesalahan ekspresi reguler" +regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab} kata yang dibisukan:" instanceMute: "Bisuka instansi" userSaysSomething: "{name} mengatakan sesuatu" makeActive: "Aktifkan" @@ -839,7 +840,11 @@ tenMinutes: "10 Menit" oneHour: "1 Jam" oneDay: "1 Hari" oneWeek: "1 Bulan" +reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan." failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun" +rateLimitExceeded: "Batas sudah terlampaui" +cropImage: "potong gambar" +cropImageAsk: "Ingin memotong gambar?" _emailUnavailable: used: "Alamat surel ini telah digunakan" format: "Format tidak valid." @@ -1085,7 +1090,6 @@ _sfx: antenna: "Penerimaan Antenna" channel: "Pemberitahuan saluran" _ago: - unknown: "Tidak diketahui" future: "Masa depan" justNow: "Baru saja" secondsAgo: "{n} detik lalu" @@ -1129,6 +1133,7 @@ _2fa: registerKey: "Daftarkan kunci keamanan baru" step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu." step2: "Lalu, pindai kode QR yang ada di layar." + step2Url: "Di aplikasi desktop, masukkan URL berikut:" step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu." securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu." @@ -1613,6 +1618,7 @@ _notification: yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" youWereInvitedToGroup: "Telah diundang ke grup" pollEnded: "Hasil Kuesioner telah keluar" + emptyPushNotificationMessage: "Pembaruan notifikasi dorong" _types: all: "Semua" follow: "Ikuti" @@ -1627,6 +1633,10 @@ _notification: followRequestAccepted: "Permintaan mengikuti disetujui" groupInvited: "Diundang ke grup" app: "Pemberitahuan dari aplikasi" + _actions: + followBack: "Ikuti Kembali" + reply: "Balas" + renote: "Renote" _deck: alwaysShowMainColumn: "Selalu tampilkan kolom utama" columnAlign: "Luruskan kolom" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 1eaa78b646..8584ed6a8e 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -10,7 +10,7 @@ password: "Password" forgotPassword: "Hai dimenticato la tua password?" fetchingAsApObject: "Recuperando dal Fediverso..." ok: "OK" -gotIt: "Capito!" +gotIt: "Ho capito" cancel: "Annulla" enterUsername: "Inserisci un nome utente" renotedBy: "Rinotato da {user}" @@ -767,6 +767,7 @@ customCss: "CSS personalizzato" global: "Federata" squareAvatars: "Mostra l'immagine del profilo come quadrato" sent: "Inviare" +received: "Ricevuto" searchResult: "Risultati della Ricerca" hashtags: "Hashtag" troubleshooting: "Risoluzione problemi" @@ -804,6 +805,10 @@ welcomeBackWithName: "Bentornato/a, {name}" clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email." searchByGoogle: "Cerca" indefinitely: "Non scade" +tenMinutes: "10 minuti" +oneHour: "1 ora" +oneDay: "1 giorno" +oneWeek: "1 settimana" _emailUnavailable: used: "Email giร  in uso" format: "Formato email non valido" @@ -999,7 +1004,6 @@ _sfx: antenna: "Ricezione dell'antenna" channel: "Notifiche di canale" _ago: - unknown: "Sconosciuto" future: "Futuro" justNow: "Ora" secondsAgo: "{n}s fa" @@ -1433,6 +1437,9 @@ _notification: followRequestAccepted: "Richiesta di follow accettata" groupInvited: "Invito a un gruppo" app: "Notifiche da applicazioni" + _actions: + reply: "Rispondi" + renote: "Rinota" _deck: alwaysShowMainColumn: "Mostra sempre la colonna principale" columnAlign: "Allineare colonne" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6326094dd8..43ab7f2d69 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -356,7 +356,7 @@ antennaExcludeKeywords: "้™คๅค–ใ‚ญใƒผใƒฏใƒผใƒ‰" antennaKeywordsDescription: "ใ‚นใƒšใƒผใ‚นใงๅŒบๅˆ‡ใ‚‹ใจANDๆŒ‡ๅฎšใซใชใ‚Šใ€ๆ”น่กŒใงๅŒบๅˆ‡ใ‚‹ใจORๆŒ‡ๅฎšใซใชใ‚Šใพใ™" notifyAntenna: "ๆ–ฐใ—ใ„ใƒŽใƒผใƒˆใ‚’้€š็Ÿฅใ™ใ‚‹" withFileAntenna: "ใƒ•ใ‚กใ‚คใƒซใŒๆทปไป˜ใ•ใ‚ŒใŸใƒŽใƒผใƒˆใฎใฟ" -enableServiceworker: "ServiceWorkerใ‚’ๆœ‰ๅŠนใซใ™ใ‚‹" +enableServiceworker: "ใƒ–ใƒฉใ‚ฆใ‚ถใธใฎใƒ—ใƒƒใ‚ทใƒฅ้€š็Ÿฅใ‚’ๆœ‰ๅŠนใซใ™ใ‚‹" antennaUsersDescription: "ใƒฆใƒผใ‚ถใƒผๅใ‚’ๆ”น่กŒใงๅŒบๅˆ‡ใฃใฆๆŒ‡ๅฎšใ—ใพใ™" caseSensitive: "ๅคงๆ–‡ๅญ—ๅฐๆ–‡ๅญ—ใ‚’ๅŒบๅˆฅใ™ใ‚‹" withReplies: "่ฟ”ไฟกใ‚’ๅซใ‚€" @@ -425,7 +425,7 @@ quoteQuestion: "ๅผ•็”จใจใ—ใฆๆทปไป˜ใ—ใพใ™ใ‹๏ผŸ" noMessagesYet: "ใพใ ใƒใƒฃใƒƒใƒˆใฏใ‚ใ‚Šใพใ›ใ‚“" newMessageExists: "ๆ–ฐใ—ใ„ใƒกใƒƒใ‚ปใƒผใ‚ธใŒใ‚ใ‚Šใพใ™" onlyOneFileCanBeAttached: "ใƒกใƒƒใ‚ปใƒผใ‚ธใซๆทปไป˜ใงใใ‚‹ใƒ•ใ‚กใ‚คใƒซใฏใฒใจใคใงใ™" -signinRequired: "ใƒญใ‚ฐใ‚คใƒณใ—ใฆใใ ใ•ใ„" +signinRequired: "็ถš่กŒใ™ใ‚‹ๅ‰ใซใ€ใ‚ตใ‚คใƒณใ‚ขใƒƒใƒ—ใพใŸใฏใ‚ตใ‚คใƒณใ‚คใƒณใŒๅฟ…่ฆใงใ™" invitations: "ๆ‹›ๅพ…" invitationCode: "ๆ‹›ๅพ…ใ‚ณใƒผใƒ‰" checking: "็ขบ่ชใ—ใฆใ„ใพใ™" @@ -842,6 +842,9 @@ oneDay: "1ๆ—ฅ" oneWeek: "1้€ฑ้–“" reflectMayTakeTime: "ๅๆ˜ ใ•ใ‚Œใ‚‹ใพใงๆ™‚้–“ใŒใ‹ใ‹ใ‚‹ๅ ดๅˆใŒใ‚ใ‚Šใพใ™ใ€‚" failedToFetchAccountInformation: "ใ‚ขใ‚ซใ‚ฆใƒณใƒˆๆƒ…ๅ ฑใฎๅ–ๅพ—ใซๅคฑๆ•—ใ—ใพใ—ใŸ" +rateLimitExceeded: "ใƒฌใƒผใƒˆๅˆถ้™ใ‚’่ถ…ใˆใพใ—ใŸ" +cropImage: "็”ปๅƒใฎใ‚ฏใƒญใƒƒใƒ—" +cropImageAsk: "็”ปๅƒใ‚’ใ‚ฏใƒญใƒƒใƒ—ใ—ใพใ™ใ‹๏ผŸ" _emailUnavailable: used: "ๆ—ขใซไฝฟ็”จใ•ใ‚Œใฆใ„ใพใ™" @@ -1110,7 +1113,6 @@ _sfx: channel: "ใƒใƒฃใƒณใƒใƒซ้€š็Ÿฅ" _ago: - unknown: "่ฌŽ" future: "ๆœชๆฅ" justNow: "ใŸใฃใŸไปŠ" secondsAgo: "{n}็ง’ๅ‰" @@ -1157,6 +1159,7 @@ _2fa: registerKey: "ใ‚ญใƒผใ‚’็™ป้Œฒ" step1: "ใพใšใ€{a}ใ‚„{b}ใชใฉใฎ่ช่จผใ‚ขใƒ—ใƒชใ‚’ใŠไฝฟใ„ใฎใƒ‡ใƒใ‚คใ‚นใซใ‚คใƒณใ‚นใƒˆใƒผใƒซใ—ใพใ™ใ€‚" step2: "ๆฌกใซใ€่กจ็คบใ•ใ‚Œใฆใ„ใ‚‹QRใ‚ณใƒผใƒ‰ใ‚’ใ‚ขใƒ—ใƒชใงใ‚นใ‚ญใƒฃใƒณใ—ใพใ™ใ€‚" + step2Url: "ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใƒ—ใƒชใงใฏๆฌกใฎURLใ‚’ๅ…ฅๅŠ›ใ—ใพใ™:" step3: "ใ‚ขใƒ—ใƒชใซ่กจ็คบใ•ใ‚Œใฆใ„ใ‚‹ใƒˆใƒผใ‚ฏใƒณใ‚’ๅ…ฅๅŠ›ใ—ใฆๅฎŒไบ†ใงใ™ใ€‚" step4: "ใ“ใ‚Œใ‹ใ‚‰ใƒญใ‚ฐใ‚คใƒณใ™ใ‚‹ใจใใ‚‚ใ€ๅŒใ˜ใ‚ˆใ†ใซใƒˆใƒผใ‚ฏใƒณใ‚’ๅ…ฅๅŠ›ใ—ใพใ™ใ€‚" securityKeyInfo: "FIDO2ใ‚’ใ‚ตใƒใƒผใƒˆใ™ใ‚‹ใƒใƒผใƒ‰ใ‚ฆใ‚งใ‚ขใ‚ปใ‚ญใƒฅใƒชใƒ†ใ‚ฃใ‚ญใƒผใ‚‚ใ—ใใฏ็ซฏๆœซใฎๆŒ‡็ด‹่ช่จผใ‚„PINใ‚’ไฝฟ็”จใ—ใฆใƒญใ‚ฐใ‚คใƒณใ™ใ‚‹ใ‚ˆใ†ใซ่จญๅฎšใงใใพใ™ใ€‚" @@ -1668,8 +1671,9 @@ _notification: youWereFollowed: "ใƒ•ใ‚ฉใƒญใƒผใ•ใ‚Œใพใ—ใŸ" youReceivedFollowRequest: "ใƒ•ใ‚ฉใƒญใƒผใƒชใ‚ฏใ‚จใ‚นใƒˆใŒๆฅใพใ—ใŸ" yourFollowRequestAccepted: "ใƒ•ใ‚ฉใƒญใƒผใƒชใ‚ฏใ‚จใ‚นใƒˆใŒๆ‰ฟ่ชใ•ใ‚Œใพใ—ใŸ" - youWereInvitedToGroup: "ใ‚ฐใƒซใƒผใƒ—ใซๆ‹›ๅพ…ใ•ใ‚Œใพใ—ใŸ" + youWereInvitedToGroup: "{userName}ใŒใ‚ใชใŸใ‚’ใ‚ฐใƒซใƒผใƒ—ใซๆ‹›ๅพ…ใ—ใพใ—ใŸ" pollEnded: "ใ‚ขใƒณใ‚ฑใƒผใƒˆใฎ็ตๆžœใŒๅ‡บใพใ—ใŸ" + emptyPushNotificationMessage: "ใƒ—ใƒƒใ‚ทใƒฅ้€š็Ÿฅใฎๆ›ดๆ–ฐใ‚’ใ—ใพใ—ใŸ" _types: all: "ใ™ในใฆ" @@ -1686,6 +1690,11 @@ _notification: groupInvited: "ใ‚ฐใƒซใƒผใƒ—ใซๆ‹›ๅพ…ใ•ใ‚ŒใŸ" app: "้€ฃๆบใ‚ขใƒ—ใƒชใ‹ใ‚‰ใฎ้€š็Ÿฅ" + _actions: + followBack: "ใƒ•ใ‚ฉใƒญใƒผใƒใƒƒใ‚ฏ" + reply: "่ฟ”ไฟก" + renote: "Renote" + _deck: alwaysShowMainColumn: "ๅธธใซใƒกใ‚คใƒณใ‚ซใƒฉใƒ ใ‚’่กจ็คบ" columnAlign: "ใ‚ซใƒฉใƒ ใฎๅฏ„ใ›" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 52ecd8c24e..5458152dda 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -799,7 +799,6 @@ _sfx: notification: "้€š็Ÿฅ" chat: "ใƒใƒฃใƒƒใƒˆ" _ago: - unknown: "ใ‚ใ‹ใ‚‰ใ‚“" future: "ๆœชๆฅ" justNow: "ใŸใฃใŸไปŠ" secondsAgo: "{n}็ง’ๅ‰" @@ -1202,6 +1201,9 @@ _notification: reaction: "ใƒชใ‚ขใ‚ฏใ‚ทใƒงใƒณ" receiveFollowRequest: "ใƒ•ใ‚ฉใƒญใƒผ่จฑๅฏใ—ใฆใปใ—ใ„ใฟใŸใ„ใ‚„ใง" followRequestAccepted: "ใƒ•ใ‚ฉใƒญใƒผใŒๅ—็†ใ•ใ‚ŒใŸใง" + _actions: + reply: "่ฟ”ไบ‹" + renote: "Renote" _deck: alwaysShowMainColumn: "ใ„ใคใ‚‚ใƒกใ‚คใƒณใ‚ซใƒฉใƒ ใ‚’่กจ็คบ" columnAlign: "ใ‚ซใƒฉใƒ ใฎๅฏ„ใ›" diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index 6a14cbe1ba..77ca824528 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -116,6 +116,8 @@ _notification: _types: follow: "Ig แนญแนญafaแน›" mention: "Bder" + _actions: + reply: "Err" _deck: _columns: notifications: "Ilษฃuyen" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index 3111c90dd5..3682277175 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -76,6 +76,8 @@ _profile: username: "เฒฌเฒณเฒ•เณ†เฒนเณ†เฒธเฒฐเณ" _notification: youWereFollowed: "เฒนเฒฟเฒ‚เฒฌเฒพเฒฒเฒฟเฒธเฒฟเฒฆเฒฐเณ" + _actions: + reply: "เฒ‰เฒคเณเฒคเฒฐเฒฟเฒธเณ" _deck: _columns: notifications: "เฒ…เฒงเฒฟเฒธเณ‚เฒšเฒจเณ†เฒ—เฒณเณ" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index e1ad77cbc9..4e7369a5ef 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -592,6 +592,8 @@ smtpSecure: "SMTP ์—ฐ๊ฒฐ์— Implicit SSL/TTS ์‚ฌ์šฉ" smtpSecureInfo: "STARTTLS ์‚ฌ์šฉ ์‹œ์—๋Š” ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค." testEmail: "์ด๋ฉ”์ผ ์ „์†ก ํ…Œ์ŠคํŠธ" wordMute: "๋‹จ์–ด ๋ฎคํŠธ" +regexpError: "์ •๊ทœ ํ‘œํ˜„์‹ ์˜ค๋ฅ˜" +regexpErrorDescription: "{tab}๋‹จ์–ด ๋ฎคํŠธ {line}ํ–‰์˜ ์ •๊ทœ ํ‘œํ˜„์‹์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค:" instanceMute: "์ธ์Šคํ„ด์Šค ๋ฎคํŠธ" userSaysSomething: "{name}๋‹˜์ด ๋ฌด์–ธ๊ฐ€๋ฅผ ๋งํ–ˆ์Šต๋‹ˆ๋‹ค" makeActive: "ํ™œ์„ฑํ™”" @@ -825,8 +827,21 @@ overridedDeviceKind: "์žฅ์น˜ ์œ ํ˜•" smartphone: "์Šค๋งˆํŠธํฐ" tablet: "ํƒœ๋ธ”๋ฆฟ" auto: "์ž๋™" +themeColor: "ํ…Œ๋งˆ ์ปฌ๋Ÿฌ" +size: "ํฌ๊ธฐ" +numberOfColumn: "ํ•œ ์ค„์— ๋ณด์ผ ๋ฆฌ์•ก์…˜์˜ ์ˆ˜" searchByGoogle: "๊ฒ€์ƒ‰" +instanceDefaultLightTheme: "์ธ์Šคํ„ด์Šค ๊ธฐ๋ณธ ๋ผ์ดํŠธ ํ…Œ๋งˆ" +instanceDefaultDarkTheme: "์ธ์Šคํ„ด์Šค ๊ธฐ๋ณธ ๋‹คํฌ ํ…Œ๋งˆ" +instanceDefaultThemeDescription: "๊ฐ์ฒด ํ˜•์‹์˜ ํ…Œ๋งˆ ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”." +mutePeriod: "๋ฎคํŠธํ•  ๊ธฐ๊ฐ„" indefinitely: "๋ฌด๊ธฐํ•œ" +tenMinutes: "10๋ถ„" +oneHour: "1์‹œ๊ฐ„" +oneDay: "1์ผ" +oneWeek: "์ผ์ฃผ์ผ" +reflectMayTakeTime: "๋ฐ˜์˜๋˜๊ธฐ๊นŒ์ง€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." +failedToFetchAccountInformation: "๊ณ„์ • ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค" _emailUnavailable: used: "์ด ๋ฉ”์ผ ์ฃผ์†Œ๋Š” ์‚ฌ์šฉ์ค‘์ž…๋‹ˆ๋‹ค" format: "ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค" @@ -1072,7 +1087,6 @@ _sfx: antenna: "์•ˆํ…Œ๋‚˜ ์ˆ˜์‹ " channel: "์ฑ„๋„ ์•Œ๋ฆผ" _ago: - unknown: "์•Œ ์ˆ˜ ์—†์Œ" future: "๋ฏธ๋ž˜" justNow: "๋ฐฉ๊ธˆ ์ „" secondsAgo: "{n}์ดˆ ์ „" @@ -1116,6 +1130,7 @@ _2fa: registerKey: "ํ‚ค๋ฅผ ๋“ฑ๋ก" step1: "๋จผ์ €, {a}๋‚˜ {b}๋“ฑ์˜ ์ธ์ฆ ์•ฑ์„ ์‚ฌ์šฉ ์ค‘์ธ ๋””๋ฐ”์ด์Šค์— ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค." step2: "๊ทธ ํ›„, ํ‘œ์‹œ๋˜์–ด ์žˆ๋Š” QR์ฝ”๋“œ๋ฅผ ์•ฑ์œผ๋กœ ์Šค์บ”ํ•ฉ๋‹ˆ๋‹ค." + step2Url: "๋ฐ์Šคํฌํ†ฑ ์•ฑ์—์„œ๋Š” ๋‹ค์Œ URL์„ ์ž…๋ ฅํ•˜์„ธ์š”:" step3: "์•ฑ์— ํ‘œ์‹œ๋œ ํ† ํฐ์„ ์ž…๋ ฅํ•˜์‹œ๋ฉด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค." step4: "๋‹ค์Œ ๋กœ๊ทธ์ธ๋ถ€ํ„ฐ๋Š” ํ† ํฐ์„ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." securityKeyInfo: "FIDO2๋ฅผ ์ง€์›ํ•˜๋Š” ํ•˜๋“œ์›จ์–ด ๋ณด์•ˆ ํ‚ค ํ˜น์€ ๋””๋ฐ”์ด์Šค์˜ ์ง€๋ฌธ์ธ์‹์ด๋‚˜ ํ™”๋ฉด์ž ๊ธˆ PIN์„ ์ด์šฉํ•ด์„œ ๋กœ๊ทธ์ธํ•˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." @@ -1249,7 +1264,7 @@ _profile: youCanIncludeHashtags: "ํ•ด์‹œ ํƒœ๊ทธ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." metadata: "์ถ”๊ฐ€ ์ •๋ณด" metadataEdit: "์ถ”๊ฐ€ ์ •๋ณด ํŽธ์ง‘" - metadataDescription: "ํ”„๋กœํ•„์— ์ตœ๋Œ€ 4๊ฐœ์˜ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์–ด์š”" + metadataDescription: "ํ”„๋กœํ•„์— ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์–ด์š”" metadataLabel: "๋ผ๋ฒจ" metadataContent: "๋‚ด์šฉ" changeAvatar: "์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ" @@ -1599,6 +1614,8 @@ _notification: youReceivedFollowRequest: "์ƒˆ๋กœ์šด ํŒ”๋กœ์šฐ ์š”์ฒญ์ด ์žˆ์Šต๋‹ˆ๋‹ค" yourFollowRequestAccepted: "ํŒ”๋กœ์šฐ ์š”์ฒญ์ด ์ˆ˜๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" youWereInvitedToGroup: "๊ทธ๋ฃน์— ์ดˆ๋Œ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + pollEnded: "ํˆฌํ‘œ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐœํ‘œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + emptyPushNotificationMessage: "ํ‘ธ์‹œ ์•Œ๋ฆผ์ด ๊ฐฑ์‹ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" _types: all: "์ „๋ถ€" follow: "ํŒ”๋กœ์ž‰" @@ -1608,10 +1625,15 @@ _notification: quote: "์ธ์šฉ" reaction: "๋ฆฌ์•ก์…˜" pollVote: "ํˆฌํ‘œ ์ฐธ์—ฌ" + pollEnded: "ํˆฌํ‘œ๊ฐ€ ์ข…๋ฃŒ๋จ" receiveFollowRequest: "ํŒ”๋กœ์šฐ ์š”์ฒญ์„ ๋ฐ›์•˜์„ ๋•Œ" followRequestAccepted: "ํŒ”๋กœ์šฐ ์š”์ฒญ์ด ์Šน์ธ๋˜์—ˆ์„ ๋•Œ" groupInvited: "๊ทธ๋ฃน์— ์ดˆ๋Œ€๋˜์—ˆ์„ ๋•Œ" app: "์—ฐ๋™๋œ ์•ฑ์„ ํ†ตํ•œ ์•Œ๋ฆผ" + _actions: + followBack: "ํŒ”๋กœ์šฐ" + reply: "๋‹ต๊ธ€" + renote: "Renote" _deck: alwaysShowMainColumn: "๋ฉ”์ธ ์นผ๋Ÿผ ํ•ญ์ƒ ํ‘œ์‹œ" columnAlign: "์นผ๋Ÿผ ์ •๋ ฌ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index f4e4a62182..0ded573948 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -303,6 +303,8 @@ muteThread: "Discussies dempen " unmuteThread: "Dempen van discussie ongedaan maken" hide: "Verbergen" searchByGoogle: "Zoeken" +cropImage: "Afbeelding bijsnijden" +cropImageAsk: "Bijsnijdengevraagd" _email: _follow: title: "volgde jou" @@ -371,6 +373,9 @@ _notification: renote: "Herdelen" quote: "Quote" reaction: "Reacties" + _actions: + reply: "Antwoord" + renote: "Herdelen" _deck: _columns: notifications: "Meldingen" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 78d86dd7e3..fa1dad2173 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -946,7 +946,6 @@ _sfx: chatBg: "Rozmowy (tล‚o)" channel: "Powiadomienia kanaล‚u" _ago: - unknown: "Nieznane" future: "W przyszล‚oล›ci" justNow: "Przed chwilฤ…" secondsAgo: "{n} sek. temu" @@ -1401,6 +1400,9 @@ _notification: followRequestAccepted: "Przyjฤ™to proล›bฤ™ o moลผliwoล›ฤ‡ obserwacji" groupInvited: "Zaproszono do grup" app: "Powiadomienia z aplikacji" + _actions: + reply: "Odpowiedz" + renote: "Udostฤ™pnij" _deck: alwaysShowMainColumn: "Zawsze pokazuj gล‚รณwnฤ… kolumnฤ™" columnAlign: "Wyrรณwnaj kolumny" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 104e4ceb7c..0dc15a27bb 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -37,26 +37,117 @@ favorites: "Favoritar" unfavorite: "Remover dos favoritos" favorited: "Adicionado aos favoritos." alreadyFavorited: "Jรก adicionado aos favoritos." +cantFavorite: "Nรฃo foi possรญvel adicionar aos favoritos." +pin: "Afixar no perfil" +unpin: "Desafixar do perfil" +copyContent: "Copiar conteรบdos" +copyLink: "Copiar hiperligaรงรฃo" +delete: "Eliminar" +deleteAndEdit: "Eliminar e editar" +deleteAndEditConfirm: "Tens a certeza que pretendes eliminar esta nota e editรก-la? Irรกs perder todas as suas reaรงรตes, renotas e respostas." +addToList: "Adicionar a lista" +sendMessage: "Enviar uma mensagem" +copyUsername: "Copiar nome de utilizador" +searchUser: "Pesquisar utilizador" +reply: "Responder" +loadMore: "Carregar mais" showMore: "Ver mais" youGotNewFollower: "Vocรช tem um novo seguidor" +receiveFollowRequest: "Pedido de seguimento recebido" followRequestAccepted: "Pedido de seguir aceito" +mention: "Menรงรฃo" +mentions: "Menรงรตes" +directNotes: "Notas diretas" +importAndExport: "Importar/Exportar" +import: "Importar" +export: "Exportar" +files: "Ficheiros" +download: "Descarregar" +driveFileDeleteConfirm: "Tens a certeza que pretendes apagar o ficheiro \"{name}\"? As notas que tenham este ficheiro anexado serรฃo tambรฉm apagadas." +unfollowConfirm: "Tens a certeza que queres deixar de seguir {name}?" +exportRequested: "Pediste uma exportaรงรฃo. Este processo pode demorar algum tempo. Serรก adicionado ร  tua Drive apรณs a conclusรฃo do processo." +importRequested: "Pediste uma importaรงรฃo. Este processo pode demorar algum tempo." +lists: "Listas" +noLists: "Nรฃo tens nenhuma lista" note: "Post" notes: "Posts" +following: "Seguindo" +followers: "Seguidores" +followsYou: "Segue-te" +createList: "Criar lista" +manageLists: "Gerir listas" +error: "Erro" +somethingHappened: "Ocorreu um erro" +retry: "Tentar novamente" +pageLoadError: "Ocorreu um erro ao carregar a pรกgina." +pageLoadErrorDescription: "Isto รฉ normalmente causado por erros de rede ou pela cache do browser. Experimenta limpar a cache e tenta novamente apรณs algum tempo." +serverIsDead: "O servidor nรฃo estรก respondendo. Por favor espere um pouco e tente novamente." +youShouldUpgradeClient: "Para visualizar essa pรกgina, por favor recarregue-a para atualizar seu cliente." +enterListName: "Insira um nome para a lista" +privacy: "Privacidade" +makeFollowManuallyApprove: "Pedidos de seguimento precisam ser aprovados" +defaultNoteVisibility: "Visibilidade padrรฃo" +follow: "Seguindo" +followRequest: "Mandar pedido de seguimento" +followRequests: "Pedidos de seguimento" +unfollow: "Deixar de seguir" +followRequestPending: "Pedido de seguimento pendente" enterEmoji: "Inserir emoji" renote: "Repostar" renoted: "Repostado" cantRenote: "Nรฃo pode repostar" cantReRenote: "Nรฃo pode repostar este repost" +quote: "Citar" pinnedNote: "Post fixado" +pinned: "Afixar no perfil" +you: "Vocรช" +clickToShow: "Clique para ver" sensitive: "Conteรบdo sensรญvel" +add: "Adicionar" +reaction: "Reaรงรตes" +reactionSetting: "Quais reaรงรตes a mostrar no selecionador de reaรงรตes" +rememberNoteVisibility: "Lembrar das configuraรงรตes de visibilidade de notas" +attachCancel: "Remover anexo" +markAsSensitive: "Marcar como sensรญvel" +unmarkAsSensitive: "Desmarcar como sensรญvel" +enterFileName: "Digite o nome do ficheiro" mute: "Silenciar" unmute: "Dessilenciar" +block: "Bloquear" +unblock: "Desbloquear" +suspend: "Suspender" +unsuspend: "Cancelar suspensรฃo" +blockConfirm: "Tem certeza que gostaria de bloquear essa conta?" +unblockConfirm: "Tem certeza que gostaria de desbloquear essa conta?" +suspendConfirm: "Tem certeza que gostaria de suspender essa conta?" +unsuspendConfirm: "Tem certeza que gostaria de cancelar a suspensรฃo dessa conta?" +selectList: "Escolhe uma lista" +selectAntenna: "Escolhe uma antena" +selectWidget: "Escolhe um widget" +editWidgets: "Editar widgets" +editWidgetsExit: "Pronto" +customEmojis: "Emoji personalizado" +emoji: "Emoji" +emojis: "Emojis" +emojiName: "Nome do Emoji" +emojiUrl: "URL do Emoji" +addEmoji: "Adicionar um Emoji" settingGuide: "Guia de configuraรงรฃo" +flagAsBot: "Marcar conta como robรด" +flagAsCat: "Marcar conta como gato" +flagAsCatDescription: "Ative essa opรงรฃo para marcar essa conta como gato." +flagShowTimelineReplies: "Mostrar respostas na linha de tempo" +general: "Geral" +wallpaper: "Papel de parede" +searchWith: "Buscar: {q}" +youHaveNoLists: "Nรฃo tem nenhuma lista" +followConfirm: "Tem certeza que quer deixar de seguir {name}?" instances: "Instรขncia" registeredAt: "Registrado em" perHour: "por hora" perDay: "por dia" noUsers: "Sem usuรกrios" +remove: "Eliminar" messageRead: "Lida" lightThemes: "Tema claro" darkThemes: "Tema escuro" @@ -64,6 +155,9 @@ addFile: "Adicionar arquivo" nsfw: "Conteรบdo sensรญvel" monthX: "mรชs de {month}" pinnedNotes: "Post fixado" +userList: "Listas" +none: "Nenhum" +output: "Resultado" smtpUser: "Nome de usuรกrio" smtpPass: "Senha" user: "Usuรกrios" @@ -72,9 +166,13 @@ _email: _follow: title: "Vocรช tem um novo seguidor" _mfm: + mention: "Menรงรฃo" + quote: "Citar" + emoji: "Emoji personalizado" search: "Pesquisar" _theme: keys: + mention: "Menรงรฃo" renote: "Repostar" _sfx: note: "Posts" @@ -82,15 +180,238 @@ _sfx: _widgets: notifications: "Notificaรงรตes" timeline: "Timeline" +_cw: + show: "Carregar mais" +_visibility: + followers: "Seguidores" _profile: username: "Nome de usuรกrio" _exportOrImport: + followingList: "Seguindo" muteList: "Silenciar" + blockingList: "Bloquear" + userLists: "Listas" +_pages: + blocks: + _button: + _action: + _pushEvent: + event: "Nome do evento" + message: "Mostrar mensagem quando ativado" + variable: "Variรกvel a mandar" + no-variable: "Nenhum" + callAiScript: "Invocar AiScript" + _callAiScript: + functionName: "Nome da funรงรฃo" + radioButton: "Escolha" + _radioButton: + values: "Lista de escolhas separadas por quebras de texto" + script: + categories: + logical: "Operaรงรฃo lรณgica" + operation: "Cรกlculos" + comparison: "Comparaรงรฃo" + list: "Listas" + blocks: + _strReplace: + arg2: "Texto que irรก ser substituรญdo" + arg3: "Substituir com" + strReverse: "Virar texto" + join: "Sequรชncia de texto" + _join: + arg1: "Listas" + arg2: "Separador" + add: "Somar" + _add: + arg1: "A" + arg2: "B" + subtract: "Subtrair" + _subtract: + arg1: "A" + arg2: "B" + multiply: "Multiplicar" + _multiply: + arg1: "A" + arg2: "B" + divide: "Dividir" + _divide: + arg1: "A" + arg2: "B" + mod: "O resto de" + _mod: + arg1: "A" + arg2: "B" + round: "Arredondar decimal" + _round: + arg1: "Numรฉrico" + eq: "A e B sรฃo iguais" + _eq: + arg1: "A" + arg2: "B" + notEq: "A e B sรฃo diferentes" + _notEq: + arg1: "A" + arg2: "B" + and: "A e B" + _and: + arg1: "A" + arg2: "B" + or: "A OU B" + _or: + arg1: "A" + arg2: "B" + lt: "< A รฉ menor do que B" + _lt: + arg1: "A" + arg2: "B" + gt: "> A รฉ maior do que B" + _gt: + arg1: "A" + arg2: "B" + ltEq: "<= A รฉ maior ou igual a B" + _ltEq: + arg1: "A" + arg2: "B" + gtEq: ">= A รฉ maior ou igual a B" + _gtEq: + arg1: "A" + arg2: "B" + if: "Galho" + _if: + arg1: "Se" + arg2: "Entรฃo" + arg3: "Se nรฃo" + not: "NรƒO" + _not: + arg1: "NรƒO" + random: "Aleatรณrio" + _random: + arg1: "Probabilidade" + rannum: "Numeral aleatรณrio" + _rannum: + arg1: "Valor mรญnimo" + arg2: "Valor mรกximo" + randomPick: "Escolher aleatoriamente de uma lista" + _randomPick: + arg1: "Listas" + dailyRandom: "Aleatรณrio (Muda uma vez por dia para cada usuรกrio)" + _dailyRandom: + arg1: "Probabilidade" + dailyRannum: "Numeral aleatรณrio (Muda uma vez por dia para cada usuรกrio)" + _dailyRannum: + arg1: "Valor mรญnimo" + arg2: "Valor mรกximo" + dailyRandomPick: "Escolher aleatoriamente de uma lista (Muda uma vez por dia para cada usuรกrio)" + _dailyRandomPick: + arg1: "Listas" + seedRandom: "Aleatรณrio (com semente)" + _seedRandom: + arg1: "Semente" + arg2: "Probabilidade" + seedRannum: "Nรบmero aleatรณrio (com semente)" + _seedRannum: + arg1: "Semente" + arg2: "Valor mรญnimo" + arg3: "Valor mรกximo" + seedRandomPick: "Escolher aleatoriamente de uma lista (com uma semente)" + _seedRandomPick: + arg1: "Semente" + arg2: "Listas" + DRPWPM: "Escolher aleatoriamente de uma lista ponderada (Muda uma vez por dia para cada usuรกrio)" + _DRPWPM: + arg1: "Lista de texto" + pick: "Escolhe a partir da lista" + _pick: + arg1: "Listas" + arg2: "Posiรงรฃo" + listLen: "Pegar comprimento da lista" + _listLen: + arg1: "Listas" + number: "Numรฉrico" + stringToNumber: "Texto para numรฉrico" + _stringToNumber: + arg1: "Texto" + numberToString: "Numรฉrico para texto" + _numberToString: + arg1: "Numรฉrico" + splitStrByLine: "Dividir texto por quebras" + _splitStrByLine: + arg1: "Texto" + ref: "Variรกvel" + aiScriptVar: "Variรกvel AiScript" + fn: "Funรงรฃo" + _fn: + slots: "Espaรงos" + slots-info: "Separar cada espaรงo com uma quebra de texto" + arg1: "Resultado" + for: "Repetiรงรฃo 'for'" + _for: + arg1: "Nรบmero de repetiรงรตes" + arg2: "Aรงรฃo" + typeError: "Espaรงo {slot} aceita valores de tipo \"{expect}\", mas o valor dado รฉ do tipo \"{actual}\"!" + thereIsEmptySlot: "O espaรงo {slot} estรก vazio!" + types: + string: "Texto" + number: "Numรฉrico" + array: "Listas" + stringArray: "Lista de texto" + emptySlot: "Espaรงo vazio" + enviromentVariables: "Variรกveis de ambiente" + pageVariables: "Variรกveis de pรกgina" +_relayStatus: + requesting: "Pendente" + accepted: "Aprovado" + rejected: "Recusado" _notification: + fileUploaded: "Carregamento de arquivo efetuado com sucesso" + youGotMention: "{name} te mencionou" + youGotReply: "{name} te respondeu" + youGotQuote: "{name} te citou" + youGotPoll: "{name} votou em sua enquete" + youGotMessagingMessageFromUser: "{name} te mandou uma mensagem de bate-papo" + youGotMessagingMessageFromGroup: "Uma mensagem foi mandada para o grupo {name}" youWereFollowed: "Vocรช tem um novo seguidor" + youReceivedFollowRequest: "Vocรช recebeu um pedido de seguimento" + yourFollowRequestAccepted: "Seu pedido de seguimento foi aceito" + youWereInvitedToGroup: "{userName} te convidou para um grupo" + pollEnded: "Os resultados da enquete agora estรฃo disponรญveis" + emptyPushNotificationMessage: "As notificaรงรตes de alerta foram atualizadas" _types: + all: "Todos" + follow: "Seguindo" + mention: "Menรงรฃo" + reply: "Respostas" + renote: "Repostar" + quote: "Citar" + reaction: "Reaรงรตes" + pollVote: "Votaรงรตes em enquetes" + pollEnded: "Enquetes terminando" + receiveFollowRequest: "Recebeu pedidos de seguimento" + followRequestAccepted: "Aceitou pedidos de seguimento" + groupInvited: "Convites de grupo" + app: "Notificaรงรตes de aplicativos conectados" + _actions: + followBack: "te seguiu de volta" + reply: "Responder" renote: "Repostar" _deck: + alwaysShowMainColumn: "Sempre mostrar a coluna principal" + columnAlign: "Alinhar colunas" + columnMargin: "Margem entre colunas" + columnHeaderHeight: "Altura do cabeรงalho de coluna" + addColumn: "Adicionar coluna" + swapLeft: "Trocar de posiรงรฃo com a coluna ร  esquerda" + swapRight: "Trocar de posiรงรฃo com a coluna ร  direita" + swapUp: "Trocar de posiรงรฃo com a coluna acima" + swapDown: "Trocar de posiรงรฃo com a coluna abaixo" + popRight: "Acoplar coluna ร  direita" + profile: "Perfil" _columns: + main: "Principal" + widgets: "Widgets" notifications: "Notificaรงรตes" tl: "Timeline" + antenna: "Antenas" + list: "Listas" + mentions: "Menรงรตes" + direct: "Notas diretas" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 6b2ff19e8e..cc74756119 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -562,13 +562,87 @@ plugins: "Pluginuri" deck: "Deck" undeck: "Pฤƒrฤƒseศ™te Deck" useBlurEffectForModal: "Foloseศ™te efect de blur pentru modale" +width: "Lฤƒลฃime" +height: "รŽnฤƒlลฃime" +large: "Mare" +medium: "Mediu" +small: "Mic" +generateAccessToken: "Genereazฤƒ token de acces" +permission: "Permisiuni" +enableAll: "Acteveazฤƒ tot" +disableAll: "Dezactiveazฤƒ tot" +tokenRequested: "Acordฤƒ acces la cont" +pluginTokenRequestedDescription: "Acest plugin va putea sฤƒ foloseascฤƒ permisiunile setate aici." +notificationType: "Tipul notificฤƒrii" +edit: "Editeazฤƒ" +useStarForReactionFallback: "Foloseศ™te โ˜… ca fallback dacฤƒ emoji-ul este necunoscut" +emailServer: "Server email" +enableEmail: "Activeazฤƒ distribuศ›ia de emailuri" +emailConfigInfo: "Folosit pentru a confirma emailul tฤƒu รฎn timpul logฤƒri dacฤƒ รฎศ›i uiศ›i parola" +email: "Email" +emailAddress: "Adresฤƒ de email" +smtpConfig: "Configurare Server SMTP" smtpHost: "Gazdฤƒ" +smtpPort: "Port" smtpUser: "Nume de utilizator" smtpPass: "Parolฤƒ" +emptyToDisableSmtpAuth: "Lasฤƒ username-ul ศ™i parola necompletate pentru a dezactiva verificarea SMTP" +smtpSecure: "Foloseศ™te SSL/TLS implicit pentru conecศ›iunile SMTP" +smtpSecureInfo: "Opreศ™te opศ›iunea asta dacฤƒ STARTTLS este folosit" +testEmail: "Testeazฤƒ livrarea emailurilor" +wordMute: "Cuvinte pe mut" +regexpError: "Eroare de Expresie Regulatฤƒ" +regexpErrorDescription: "A apฤƒrut o eroare รฎn expresia regulatฤƒ pe linia {line} al cuvintelor {tab} setate pe mut:" +instanceMute: "Instanศ›e pe mut" +userSaysSomething: "{name} a spus ceva" +makeActive: "Activeazฤƒ" +display: "Aratฤƒ" +copy: "Copiazฤƒ" +metrics: "Metrici" +overview: "Privire de ansamblu" +logs: "Log-uri" +delayed: "รŽntรขrziate" +database: "Baza de date" +channel: "Canale" +create: "Creazฤƒ" +notificationSetting: "Setฤƒri notificฤƒri" +notificationSettingDesc: "Selecteazฤƒ tipurile de notificฤƒri care sฤƒ fie arฤƒtate" +useGlobalSetting: "Foloseศ™te setฤƒrile globale" +useGlobalSettingDesc: "Dacฤƒ opศ›iunea e pornitฤƒ, notificฤƒrile contului tฤƒu vor fi folosite. Dacฤƒ e opritฤƒ, configuraศ›ia va fi individualฤƒ." +other: "Altele" +regenerateLoginToken: "Regenereazฤƒ token de login" +regenerateLoginTokenDescription: "Regenereazฤƒ token-ul folosit intern รฎn timpul logฤƒri. รŽn mod normal asta nu este necesar. Odatฤƒ regenerat, toate dispozitivele vor fi delogate." +setMultipleBySeparatingWithSpace: "Separฤƒ mai multe intrฤƒri cu spaศ›ii." +fileIdOrUrl: "Introdu ID sau URL" +behavior: "Comportament" +sample: "exemplu" +abuseReports: "Rapoarte" +reportAbuse: "Raporteazฤƒ" +reportAbuseOf: "Raporteazฤƒ {name}" +fillAbuseReportDescription: "Te rog scrie detaliile legate de acest raport. Dacฤƒ este despre o notฤƒ specificฤƒ, te rog introdu URL-ul ei." +abuseReported: "Raportul tฤƒu a fost trimis. Mulศ›umim." +reporter: "Raportorul" +reporteeOrigin: "Originea raportatului" +reporterOrigin: "Originea raportorului" +forwardReport: "Redirecศ›ioneazฤƒ raportul cฤƒtre instanศ›a externฤƒ" +forwardReportIsAnonymous: "รŽn locul contului tฤƒu, va fi afiศ™at un cont anonim, de sistem, ca raportor cฤƒtre instanศ›a externฤƒ." +send: "Trimite" +abuseMarkAsResolved: "Marcheazฤƒ raportul ca rezolvat" +openInNewTab: "Deschide รฎn tab nou" +openInSideView: "Deschide รฎn vedere lateralฤƒ" +defaultNavigationBehaviour: "Comportament de navigare implicit" +editTheseSettingsMayBreakAccount: "Editarea acestor setฤƒri รฎศ›i pot defecta contul." +waitingFor: "Aศ™teptรขnd pentru {x}" +random: "Aleator" +system: "Sistem" +switchUi: "Schimbฤƒ UI" +desktop: "Desktop" clearCache: "Goleศ™te cache-ul" info: "Despre" user: "Utilizatori" administration: "Gestionare" +middle: "Mediu" +sent: "Trimite" searchByGoogle: "Cautฤƒ" _email: _follow: @@ -641,6 +715,9 @@ _notification: renote: "Re-noteazฤƒ" quote: "Citeazฤƒ" reaction: "Reacศ›ie" + _actions: + reply: "Rฤƒspunde" + renote: "Re-noteazฤƒ" _deck: _columns: notifications: "Notificฤƒri" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 877e1e185d..c44589a7e5 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -141,6 +141,8 @@ flagAsBot: "ะะบะบะฐัƒะฝั‚ ะฑะพั‚ะฐ" flagAsBotDescription: "ะ’ะบะปัŽั‡ะธั‚ะต, ะตัะปะธ ัั‚ะพั‚ ะฐะบะบะฐัƒะฝั‚ ัƒะฟั€ะฐะฒะปัะตั‚ัั ะฟั€ะพะณั€ะฐะผะผะพะน. ะญั‚ะพ ะฟะพะทะฒะพะปะธั‚ ัะธัั‚ะตะผะต Misskey ัƒั‡ะธั‚ั‹ะฒะฐั‚ัŒ ัั‚ะพ, ะฐ ั‚ะฐะบะถะต ะฟะพะผะพะถะตั‚ ั€ะฐะทั€ะฐะฑะพั‚ั‡ะธะบะฐะผ ะดั€ัƒะณะธั… ะฑะพั‚ะพะฒ ะฟั€ะตะดะพั‚ะฒั€ะฐั‚ะธั‚ัŒ ะฑะตัะบะพะฝะตั‡ะฝั‹ะต ั†ะธะบะปั‹ ะฒะทะฐะธะผะพะดะตะนัั‚ะฒะธั." flagAsCat: "ะะบะบะฐัƒะฝั‚ ะบะพั‚ะฐ" flagAsCatDescription: "ะ’ะบะปัŽั‡ะธั‚ะต, ะธ ัั‚ะพั‚ ะฐะบะบะฐัƒะฝั‚ ะฑัƒะดะตั‚ ะฟะพะผะตั‡ะตะฝ ะบะฐะบ ะบะพัˆะฐั‡ะธะน." +flagShowTimelineReplies: "ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ะพั‚ะฒะตั‚ั‹ ะฝะฐ ะทะฐะผะตั‚ะบะธ ะฒ ะปะตะฝั‚ะต" +flagShowTimelineRepliesDescription: "ะ•ัะปะธ ัั‚ะพั‚ ะฟะฐั€ะฐะผะตั‚ั€ ะฒะบะปัŽั‡ะตะฝ, ั‚ะพ ะฒ ะปะตะฝั‚ะต, ะฒ ะดะพะฟะพะปะฝะตะฝะธะต ะบ ะทะฐะผะตั‚ะบะฐะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั, ะพั‚ะพะฑั€ะฐะถะฐัŽั‚ัั ะพั‚ะฒะตั‚ั‹ ะฝะฐ ะดั€ัƒะณะธะต ะทะฐะผะตั‚ะบะธ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั." autoAcceptFollowed: "ะŸั€ะธะฝะธะผะฐั‚ัŒ ะฟะพะดะฟะธัั‡ะธะบะพะฒ ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ" addAccount: "ะ”ะพะฑะฐะฒะธั‚ัŒ ัƒั‡ั‘ั‚ะฝัƒัŽ ะทะฐะฟะธััŒ" loginFailed: "ะะตัƒะดะฐั‡ะฝะฐั ะฟะพะฟั‹ั‚ะบะฐ ะฒั…ะพะดะฐ" @@ -236,6 +238,7 @@ saved: "ะกะพั…ั€ะฐะฝะตะฝะพ" messaging: "ะกะพะพะฑั‰ะตะฝะธั" upload: "ะ—ะฐะณั€ัƒะทะธั‚ัŒ" keepOriginalUploading: "ะกะพั…ั€ะฐะฝะธั‚ัŒ ะธัั…ะพะดะฝะพะต ะธะทะพะฑั€ะฐะถะตะฝะธะต" +keepOriginalUploadingDescription: "ะกะพั…ั€ะฐะฝัะตั‚ ะธัั…ะพะดะฝัƒัŽ ะฒะตั€ัะธัŽ ะฟั€ะธ ะทะฐะณั€ัƒะทะบะต ะธะทะพะฑั€ะฐะถะตะฝะธะน. ะ•ัะปะธ ะฒั‹ะบะปัŽั‡ะธั‚ัŒ, ั‚ะพ ะฟั€ะธ ะทะฐะณั€ัƒะทะบะต ะฑั€ะฐัƒะทะตั€ ะณะตะฝะตั€ะธั€ัƒะตั‚ ะธะทะพะฑั€ะฐะถะตะฝะธะต ะดะปั ะฟัƒะฑะปะธะบะฐั†ะธะธ." fromDrive: "ะก ยซะดะธัะบะฐยป" fromUrl: "ะŸะพ ััั‹ะปะบะต" uploadFromUrl: "ะ—ะฐะณั€ัƒะทะธั‚ัŒ ะฟะพ ััั‹ะปะบะต" @@ -589,6 +592,7 @@ smtpSecure: "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ SSL/TLS ะดะปั SMTP-ัะพะตะดะธะฝะตะฝะธะน" smtpSecureInfo: "ะ’ั‹ะบะปัŽั‡ะธั‚ะต ะฟั€ะธ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธะธ STARTTLS." testEmail: "ะŸั€ะพะฒะตั€ะบะฐ ะดะพัั‚ะฐะฒะบะธ ัะปะตะบั‚ั€ะพะฝะฝะพะน ะฟะพั‡ั‚ั‹" wordMute: "ะกะบั€ั‹ั‚ะธะต ัะปะพะฒ" +regexpError: "ะžัˆะธะฑะบะฐ ะฒ ั€ะตะณัƒะปัั€ะฝะพะผ ะฒั‹ั€ะฐะถะตะฝะธะธ" instanceMute: "ะ“ะปัƒัˆะตะฝะธะต ะธะฝัั‚ะฐะฝัะพะฒ" userSaysSomething: "{name} ั‡ั‚ะพ-ั‚ะพ ัะพะพะฑั‰ะฐะตั‚" makeActive: "ะะบั‚ะธะฒะธั€ะพะฒะฐั‚ัŒ" @@ -619,6 +623,8 @@ fillAbuseReportDescription: "ะžะฟะธัˆะธั‚ะต, ะฟะพะถะฐะปัƒะนัั‚ะฐ, ะฟั€ะธั‡ะธะฝ abuseReported: "ะ–ะฐะปะพะฑะฐ ะพั‚ะฟั€ะฐะฒะปะตะฝะฐ. ะ‘ะพะปัŒัˆะพะต ัะฟะฐัะธะฑะพ ะทะฐ ะธะฝั„ะพั€ะผะฐั†ะธัŽ." reporteeOrigin: "ะž ะบะพะผ ัะพะพะฑั‰ะตะฝะพ" reporterOrigin: "ะšั‚ะพ ัะพะพะฑั‰ะธะป" +forwardReport: "ะŸะตั€ะตะฝะฐะฟั€ะฐะฒะปะตะฝะธะต ะพั‚ั‡ะตั‚ะฐ ะฝะฐ ะธะฝัั‚ะฐะฝั‚." +forwardReportIsAnonymous: "ะฃะดะฐะปะตะฝะฝั‹ะน ะธะฝัั‚ะฐะฝั‚ ะฝะต ัะผะพะถะตั‚ ัƒะฒะธะดะตั‚ัŒ ะฒะฐัˆัƒ ะธะฝั„ะพั€ะผะฐั†ะธัŽ ะธ ะฑัƒะดะตั‚ ะพั‚ะพะฑั€ะฐะถะฐั‚ัŒัั ะบะฐะบ ะฐะฝะพะฝะธะผะฝะฐั ัะธัั‚ะตะผะฝะฐั ัƒั‡ะตั‚ะฝะฐั ะทะฐะฟะธััŒ." send: "ะžั‚ะฟั€ะฐะฒะธั‚ัŒ" abuseMarkAsResolved: "ะžั‚ะผะตั‚ะธั‚ัŒ ะถะฐะปะพะฑัƒ ะบะฐะบ ั€ะตัˆั‘ะฝะฝัƒัŽ" openInNewTab: "ะžั‚ะบั€ั‹ั‚ัŒ ะฒ ะฝะพะฒะพะน ะฒะบะปะฐะดะบะต" @@ -815,7 +821,16 @@ leaveGroupConfirm: "ะŸะพะบะธะฝัƒั‚ัŒ ะณั€ัƒะฟะฟัƒ ยซ{name}ยป?" useDrawerReactionPickerForMobile: "ะ’ั‹ะดะฒะธะถะฝะฐั ะฟะฐะปะธั‚ั€ะฐ ะฝะฐ ะผะพะฑะธะปัŒะฝะพะผ ัƒัั‚ั€ะพะนัั‚ะฒะต" welcomeBackWithName: "ะก ะฒะพะทะฒั€ะฐั‰ะตะฝะธะตะผ, {name}!" clickToFinishEmailVerification: "ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฝะฐะถะผะธั‚ะต [{ok}], ั‡ั‚ะพะฑั‹ ะทะฐะฒะตั€ัˆะธั‚ัŒ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธะต ะฐะดั€ะตัะฐ ัะปะตะบั‚ั€ะพะฝะฝะพะน ะฟะพั‡ั‚ั‹." +overridedDeviceKind: "ะขะธะฟ ัƒัั‚ั€ะพะนัั‚ะฒะฐ" +smartphone: "ะกะผะฐั€ั‚ั„ะพะฝ" +tablet: "ะŸะปะฐะฝัˆะตั‚" +auto: "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ" +themeColor: "ะฆะฒะตั‚ ั‚ะตะผั‹" +size: "ะ ะฐะทะผะตั€" +numberOfColumn: "ะšะพะปะธั‡ะตัั‚ะฒะพ ัั‚ะพะปะฑั†ะพะฒ" searchByGoogle: "ะŸะพะธัะบ" +instanceDefaultLightTheme: "ะกะฒะตั‚ะปะฐั ั‚ะตะผะฐ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ" +instanceDefaultDarkTheme: "ะขะตะผะฝะฐั ั‚ะตะผะฐ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ" indefinitely: "ะฒะตั‡ะฝะพ" _emailUnavailable: used: "ะฃะถะต ะธัะฟะพะปัŒะทัƒะตั‚ัั" @@ -1059,7 +1074,6 @@ _sfx: antenna: "ะะฝั‚ะตะฝะฝะฐ" channel: "ะšะฐะฝะฐะป" _ago: - unknown: "ะšะพะณะดะฐ-ั‚ะพ" future: "ะ˜ะท ะฑัƒะดัƒั‰ะตะณะพ" justNow: "ะขะพะปัŒะบะพ ั‡ั‚ะพ" secondsAgo: "{n} ั ะฝะฐะทะฐะด" @@ -1599,6 +1613,9 @@ _notification: followRequestAccepted: "ะ—ะฐะฟั€ะพั ะฝะฐ ะฟะพะดะฟะธัะบัƒ ะพะดะพะฑั€ะตะฝ" groupInvited: "ะŸั€ะธะณะปะฐัˆะตะฝะธะต ะฒ ะณั€ัƒะฟะฟั‹" app: "ะฃะฒะตะดะพะผะปะตะฝะธั ะธะท ะฟั€ะธะปะพะถะตะฝะธะน" + _actions: + reply: "ะžั‚ะฒะตั‚ะธั‚ัŒ" + renote: "ะ ะตะฟะพัั‚" _deck: alwaysShowMainColumn: "ะ’ัะตะณะดะฐ ะฟะพะบะฐะทั‹ะฒะฐั‚ัŒ ะณะปะฐะฒะฝัƒัŽ ะบะพะปะพะฝะบัƒ" columnAlign: "ะ’ั‹ั€ะฐะฒะฝะธะฒะฐะฝะธะต ะบะพะปะพะฝะพะบ" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index c6f2f59bdf..dc1151522e 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -841,6 +841,7 @@ oneDay: "1 deลˆ" oneWeek: "1 tรฝลพdeลˆ" reflectMayTakeTime: "Zmeny mรดลพu chvรญฤพu trvaลฅ kรฝm sa prejavia." failedToFetchAccountInformation: "Nepodarilo sa naฤรญtaลฅ informรกcie o รบฤte." +rateLimitExceeded: "Prekroฤenรฝ limit rรฝchlosti" _emailUnavailable: used: "Tรกto emailovรก adresa sa uลพ pouลพรญva" format: "Formรกt emailovej adresy je nesprรกvny" @@ -1086,7 +1087,6 @@ _sfx: antenna: "Antรฉny" channel: "Upozornenia kanรกla" _ago: - unknown: "Neznรกme" future: "Budรบcnosลฅ" justNow: "Teraz" secondsAgo: "pred {n} sekundami" @@ -1130,6 +1130,7 @@ _2fa: registerKey: "Registrovaลฅ bezpeฤnostnรฝ kฤพรบฤ" step1: "Najprv si nainลกtalujte autentifikaฤnรบ aplikรกciu (naprรญklad {a} alebo {b}) na svoje zariadenie." step2: "Potom, naskenujte QR kรณd zobrazenรฝ na obrazovke." + step2Url: "Do aplikรกcie zadajte nasledujรบcu URL adresu:" step3: "Nastavenie dokonฤรญte zadanรญm tokenu z vaลกej aplikรกcie." step4: "Od teraz, vลกetky ฤalลกie prihlรกsenia budรบ vyลพadovaลฅ prihlasovacรญ token." securityKeyInfo: "Okrem odtlaฤku prsta alebo PIN autentifikรกcie si mรดลพete nastaviลฅ autentifikรกciu cez hardvรฉrovรฝ bezpeฤnostnรฝ kฤพรบฤ podporujรบci FIDO2 a tak eลกte viac zabezpeฤiลฅ svoj รบฤet." @@ -1628,6 +1629,10 @@ _notification: followRequestAccepted: "Schvรกlenรฉ ลพiadosti o sledovanie" groupInvited: "Pozvรกnky do skupรญn" app: "Oznรกmenia z prepojenรฝch aplikรกciรญ" + _actions: + followBack: "Sledovaลฅ spรคลฅ\n" + reply: "Odpovedaลฅ" + renote: "Preposlaลฅ" _deck: alwaysShowMainColumn: "Vลพdy zobraziลฅ v hlavnom stฤบpci" columnAlign: "Zarovnaลฅ stฤบpce" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml new file mode 100644 index 0000000000..42bfa45f25 --- /dev/null +++ b/locales/sv-SE.yml @@ -0,0 +1,319 @@ +--- +_lang_: "Svenska" +headlineMisskey: "Ett nรคtverk kopplat av noter" +introMisskey: "Vรคlkommen! Misskey รคr en รถppen och decentraliserad mikrobloggningstjรคnst.\nSkapa en \"not\" och dela dina tankar med alla runtomkring dig. ๐Ÿ“ก\nMed \"reaktioner\" kan du snabbt uttrycka dina kรคnslor kring andras noter.๐Ÿ‘\nLรฅt oss utforska en nya vรคrld!๐Ÿš€" +monthAndDay: "{day}/{month}" +search: "Sรถk" +notifications: "Notifikationer" +username: "Anvรคndarnamn" +password: "Lรถsenord" +forgotPassword: "Glรถmt lรถsenord" +fetchingAsApObject: "Hรคmtar frรฅn Fediversum..." +ok: "OK" +gotIt: "Uppfattat!" +cancel: "Avbryt" +enterUsername: "Ange anvรคndarnamn" +renotedBy: "Omnoterad av {user}" +noNotes: "Inga noteringar" +noNotifications: "Inga aviseringar" +instance: "Instanser" +settings: "Instรคllningar" +basicSettings: "Basinstรคllningar" +otherSettings: "Andra instรคllningar" +openInWindow: "ร–ppna i ett fรถnster" +profile: "Profil" +timeline: "Tidslinje" +noAccountDescription: "Anvรคndaren har inte skrivit en biografi รคn." +login: "Logga in" +loggingIn: "Loggar in" +logout: "Logga ut" +signup: "Registrera" +uploading: "Uppladdning sker..." +save: "Spara" +users: "Anvรคndare" +addUser: "Lรคgg till anvรคndare" +favorite: "Lรคgg till i favoriter" +favorites: "Favoriter" +unfavorite: "Avfavorisera" +favorited: "Tillagd i favoriter." +alreadyFavorited: "Redan tillagd i favoriter." +cantFavorite: "Gick inte att lรคgga till i favoriter." +pin: "Fรคst till profil" +unpin: "Lossa frรฅn profil" +copyContent: "Kopiera innehรฅll" +copyLink: "Kopiera lรคnk" +delete: "Radera" +deleteAndEdit: "Radera och รคndra" +deleteAndEditConfirm: "ร„r du sรคker att du vill radera denna not och รคndra den? Du kommer fรถrlora alla reaktioner, omnoteringar och svar till den." +addToList: "Lรคgg till i lista" +sendMessage: "Skicka ett meddelande" +copyUsername: "Kopiera anvรคndarnamn" +searchUser: "Sรถk anvรคndare" +reply: "Svara" +loadMore: "Ladda mer" +showMore: "Visa mer" +youGotNewFollower: "fรถljde dig" +receiveFollowRequest: "Fรถljarfรถrfrรฅgan mottagen" +followRequestAccepted: "Fรถljarfรถrfrรฅgan accepterad" +mention: "Nรคmn" +mentions: "Omnรคmningar" +directNotes: "Direktnoter" +importAndExport: "Importera / Exportera" +import: "Importera" +export: "Exportera" +files: "Filer" +download: "Nedladdning" +driveFileDeleteConfirm: "ร„r du sรคker att du vill radera filen \"{name}\"? Noter med denna fil bifogad kommer ocksรฅ raderas." +unfollowConfirm: "ร„r du sรคker att du vill avfรถlja {name}?" +exportRequested: "Du har begรคrt en export. Detta kan ta lite tid. Den kommer lรคggas till i din Drive nรคr den blir klar." +importRequested: "Du har begรคrt en import. Detta kan ta lite tid." +lists: "Listor" +noLists: "Du har inga listor" +note: "Not" +notes: "Noter" +following: "Fรถljer" +followers: "Fรถljare" +followsYou: "Fรถljer dig" +createList: "Skapa lista" +manageLists: "Hantera lista" +error: "Fel!" +somethingHappened: "Ett fel har uppstรฅtt" +retry: "Fรถrsรถk igen" +pageLoadError: "Det gick inte att ladda sidan." +pageLoadErrorDescription: "Detta hรคnder oftast p.g.a. nรคtverksfel eller din webblรคsarcache. Fรถrsรถk tรถmma din cache och testa sedan igen efter en liten stund." +serverIsDead: "Servern svarar inte. Vรคnta ett litet tag och fรถrsรถk igen." +youShouldUpgradeClient: "Fรถr att kunna se denna sida, vรคnligen ladda om sidan fรถr att uppdatera din klient." +enterListName: "Skriv ett namn till listan" +privacy: "Integritet" +makeFollowManuallyApprove: "Fรถljarfรถrfrรฅgningar krรคver manuellt godkรคnnande" +defaultNoteVisibility: "Standardsynlighet" +follow: "Fรถlj" +followRequest: "Skicka fรถljarfรถrfrรฅgan" +followRequests: "Fรถljarfรถrfrรฅgningar" +unfollow: "Avfรถlj" +followRequestPending: "Fรถljarfรถrfrรฅgning avvaktar fรถr svar" +enterEmoji: "Skriv en emoji" +renote: "Omnotera" +unrenote: "Ta tillbaka omnotering" +renoted: "Omnoterad." +cantRenote: "Inlรคgget kunde inte bli omnoterat." +cantReRenote: "En omnotering kan inte bli omnoterad." +quote: "Citat" +pinnedNote: "Fรคstad not" +pinned: "Fรคst till profil" +you: "Du" +clickToShow: "Klicka fรถr att visa" +sensitive: "Kรคnsligt innehรฅll" +add: "Lรคgg till" +reaction: "Reaktioner" +reactionSetting: "Reaktioner som ska visas i reaktionsvรคljaren" +reactionSettingDescription2: "Dra fรถr att omordna, klicka fรถr att radera, tryck \"+\" fรถr att lรคgga till." +rememberNoteVisibility: "Komihรฅg notvisningsinstรคllningar" +attachCancel: "Ta bort bilaga" +markAsSensitive: "Markera som kรคnsligt innehรฅll" +unmarkAsSensitive: "Avmarkera som kรคnsligt innehรฅll" +enterFileName: "Ange filnamn" +mute: "Tysta" +unmute: "Avtysta" +block: "Blockera" +unblock: "Avblockera" +suspend: "Suspendera" +unsuspend: "Ta bort suspenderingen" +blockConfirm: "ร„r du sรคker att du vill blockera kontot?" +unblockConfirm: "ร„r du sรคkert att du vill avblockera kontot?" +suspendConfirm: "ร„r du sรคker att du vill suspendera detta konto?" +unsuspendConfirm: "ร„r du sรคker att du vill avsuspendera detta konto?" +selectList: "Vรคlj lista" +selectAntenna: "Vรคlj en antenn" +selectWidget: "Vรคlj en widget" +editWidgets: "Redigera widgets" +editWidgetsExit: "Avsluta redigering" +customEmojis: "Anpassa emoji" +emoji: "Emoji" +emojis: "Emoji" +emojiName: "Emoji namn" +emojiUrl: "Emoji lรคnk" +addEmoji: "Lรคgg till emoji" +settingGuide: "Rekommenderade instรคllningar" +cacheRemoteFiles: "Spara externa filer till cachen" +cacheRemoteFilesDescription: "Nรคr denna instรคllning รคr avstรคngd kommer externa filer laddas direkt frรฅn den externa instansen. Genom att stรคnga av detta kommer lagringsutrymme minska i anvรคndning men kommer รถka datatrafiken eftersom miniatyrer inte kommer genereras." +flagAsBot: "Markera konto som bot" +flagAsBotDescription: "Aktivera det hรคr alternativet om kontot รคr kontrollerat av ett program. Om aktiverat kommer den fungera som en flagga fรถr andra utvecklare fรถr att hindra รคndlรถsa kedjor med andra bottar. Det kommer ocksรฅ fรฅ Misskeys interna system att hantera kontot som en bot." +flagAsCat: "Markera konto som katt" +flagAsCatDescription: "Aktivera denna instรคllning fรถr att markera kontot som en katt." +flagShowTimelineReplies: "Visa svar i tidslinje" +flagShowTimelineRepliesDescription: "Visar anvรคndarsvar till andra anvรคndares noter i tidslinjen om pรฅslagen." +autoAcceptFollowed: "Godkรคnn fรถljarfรถrfrรฅgningar frรฅn anvรคndare du fรถljer automatiskt" +addAccount: "Lรคgg till konto" +loginFailed: "Inloggningen misslyckades" +showOnRemote: "Se pรฅ extern instans" +general: "Allmรคnt" +wallpaper: "Bakgrundsbild" +setWallpaper: "Vรคlj bakgrund" +removeWallpaper: "Ta bort bakgrund" +searchWith: "Sรถk: {q}" +youHaveNoLists: "Du har inga listor" +followConfirm: "ร„r du sรคker att du vill fรถlja {name}?" +proxyAccount: "Proxykonto" +proxyAccountDescription: "Ett proxykonto รคr ett konto som agerar som en extern fรถljare fรถr anvรคndare under vissa villkor. Till exempel, nรคr en anvรคndare lรคgger till en extern anvรคndare till en lista sรฅ kommer den externa anvรคndarens aktivitet inte levireras till instansen om ingen lokal anvรคndare fรถljer det kontot, sรฅ proxykontot anvรคnds istรคllet." +host: "Vรคrd" +selectUser: "Vรคlj anvรคndare" +recipient: "Mottagare" +annotation: "Kommentarer" +federation: "Federation" +instances: "Instanser" +registeredAt: "Registrerad pรฅ" +latestRequestSentAt: "Senaste fรถrfrรฅgan skickad" +latestRequestReceivedAt: "Senaste begรคran mottagen" +latestStatus: "Senaste status" +storageUsage: "Anvรคnt lagringsutrymme" +charts: "Diagram" +perHour: "Per timme" +perDay: "Per dag" +stopActivityDelivery: "Sluta skicka aktiviteter" +blockThisInstance: "Blockera instans" +operations: "Operationer" +software: "Mjukvara" +version: "Version" +metadata: "Metadata" +withNFiles: "{n} fil(er)" +monitor: "ร–vervakning" +jobQueue: "Jobbkรถ" +cpuAndMemory: "CPU och minne" +network: "Nรคtverk" +disk: "Disk" +instanceInfo: "Instansinformation" +statistics: "Statistik" +clearQueue: "Rensa kรถ" +clearQueueConfirmTitle: "ร„r du sรคker att du vill rensa kรถn?" +clearQueueConfirmText: "Om nรฅgon not รคr olevererad i kรถn kommer den inte federeras. Vanligtvis behรถvs inte denna handling." +clearCachedFiles: "Rensa cache" +clearCachedFilesConfirm: "ร„r du sรคker att du vill radera alla cachade externa filer?" +blockedInstances: "Blockerade instanser" +blockedInstancesDescription: "Lista adressnamn av instanser som du vill blockera. Listade instanser kommer inte lรคngre kommunicera med denna instans." +muteAndBlock: "Tystningar och blockeringar" +mutedUsers: "Tystade anvรคndare" +blockedUsers: "Blockerade anvรคndare" +noUsers: "Det finns inga anvรคndare" +editProfile: "Redigera profil" +noteDeleteConfirm: "ร„r du sรคker pรฅ att du vill ta bort denna not?" +pinLimitExceeded: "Du kan inte fรคsta fler noter" +intro: "Misskey har installerats! Vรคnligen skapa en adminanvรคndare." +done: "Klar" +processing: "Bearbetar..." +preview: "Fรถrhandsvisning" +default: "Standard" +noCustomEmojis: "Det finns ingen emoji" +noJobs: "Det finns inga jobb" +federating: "Federerar" +blocked: "Blockerad" +suspended: "Suspenderad" +all: "Allt" +subscribing: "Prenumererar" +publishing: "Publiceras" +notResponding: "Svarar inte" +instanceFollowing: "Fรถljer pรฅ instans" +instanceFollowers: "Fรถljare av instans" +instanceUsers: "Anvรคndare av denna instans" +changePassword: "ร„ndra lรถsenord" +security: "Sรคkerhet" +retypedNotMatch: "Inmatningen matchar inte" +currentPassword: "Nuvarande lรถsenord" +newPassword: "Nytt lรถsenord" +newPasswordRetype: "Bekrรคfta lรถsenord" +attachFile: "Bifoga filer" +more: "Mer!" +featured: "Utvalda" +usernameOrUserId: "Anvรคndarnamn eller anvรคndar-id" +noSuchUser: "Kan inte hitta anvรคndaren" +lookup: "Sรถkning" +announcements: "Nyheter" +imageUrl: "Bild-URL" +remove: "Radera" +removed: "Borttaget" +removeAreYouSure: "ร„r du sรคker att du vill radera \"{x}\"?" +deleteAreYouSure: "ร„r du sรคker att du vill radera \"{x}\"?" +resetAreYouSure: "Vill du รฅterstรคlla?" +saved: "Sparad" +messaging: "Chatt" +upload: "Ladda upp" +keepOriginalUploading: "Behรฅll originalbild" +nsfw: "Kรคnsligt innehรฅll" +pinnedNotes: "Fรคstad not" +userList: "Listor" +smtpHost: "Vรคrd" +smtpUser: "Anvรคndarnamn" +smtpPass: "Lรถsenord" +clearCache: "Rensa cache" +user: "Anvรคndare" +searchByGoogle: "Sรถk" +_email: + _follow: + title: "fรถljde dig" +_mfm: + mention: "Nรคmn" + quote: "Citat" + emoji: "Anpassa emoji" + search: "Sรถk" +_theme: + keys: + mention: "Nรคmn" + renote: "Omnotera" +_sfx: + note: "Noter" + notification: "Notifikationer" + chat: "Chatt" +_widgets: + notifications: "Notifikationer" + timeline: "Tidslinje" + federation: "Federation" + jobQueue: "Jobbkรถ" +_cw: + show: "Ladda mer" +_visibility: + followers: "Fรถljare" +_profile: + username: "Anvรคndarnamn" +_exportOrImport: + followingList: "Fรถljer" + muteList: "Tysta" + blockingList: "Blockera" + userLists: "Listor" +_charts: + federation: "Federation" +_pages: + script: + categories: + list: "Listor" + blocks: + _join: + arg1: "Listor" + _randomPick: + arg1: "Listor" + _dailyRandomPick: + arg1: "Listor" + _seedRandomPick: + arg2: "Listor" + _pick: + arg1: "Listor" + _listLen: + arg1: "Listor" + types: + array: "Listor" +_notification: + youWereFollowed: "fรถljde dig" + _types: + follow: "Fรถljer" + mention: "Nรคmn" + renote: "Omnotera" + quote: "Citat" + reaction: "Reaktioner" + _actions: + reply: "Svara" + renote: "Omnotera" +_deck: + _columns: + notifications: "Notifikationer" + tl: "Tidslinje" + list: "Listor" + mentions: "Omnรคmningar" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 073b2c310e..7e7ef8685f 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -7,6 +7,7 @@ search: "ะŸะพัˆัƒะบ" notifications: "ะกะฟะพะฒั–ั‰ะตะฝะฝั" username: "ะ†ะผ'ั ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ" password: "ะŸะฐั€ะพะปัŒ" +forgotPassword: "ะฏ ะทะฐะฑัƒะฒ ะฟะฐั€ะพะปัŒ" fetchingAsApObject: "ะžั‚ั€ะธะผัƒั”ะผะพ ะท ั„ะตะดั–ะฒะตั€ััƒ..." ok: "OK" gotIt: "ะ—ั€ะพะทัƒะผั–ะปะพ!" @@ -80,6 +81,8 @@ somethingHappened: "ะฉะพััŒ ะฟั–ัˆะปะพ ะฝะต ั‚ะฐะบ" retry: "ะกะฟั€ะพะฑัƒะฒะฐั‚ะธ ะทะฝะพะฒัƒ" pageLoadError: "ะŸะพะผะธะปะบะฐ ะฟั€ะธ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั– ัั‚ะพั€ั–ะฝะบะธ" pageLoadErrorDescription: "ะ—ะฐะทะฒะธั‡ะฐะน ั†ะต ะฟะพะฒโ€™ัะทะฐะฝะพ ะท ะฟะพะผะธะปะบะฐะผะธ ะผะตั€ะตะถั– ะฐะฑะพ ะบะตัˆะตะผ ะฑั€ะฐัƒะทะตั€ะฐ. ะžั‡ะธัั‚ั–ั‚ัŒ ะบะตัˆ ะฐะฑะพ ะฟะพั‡ะตะบะฐะนั‚ะต ั‚ั€ะพั…ะธ ะน ัะฟั€ะพะฑัƒะนั‚ะต ั‰ะต ั€ะฐะท." +serverIsDead: "ะ’ั–ะดะฟะพะฒั–ะดั– ะฒั–ะด ัะตั€ะฒะตั€ะฐ ะฝะตะผะฐั”. ะ—ะฐั‡ะตะบะฐะนั‚ะต ะดะตัะบะธะน ั‡ะฐั ั– ะฟะพะฒั‚ะพั€ั–ั‚ัŒ ัะฟั€ะพะฑัƒ." +youShouldUpgradeClient: "ะŸะตั€ะตะทะฐะฒะฐะฝั‚ะฐะถั‚ะต ั‚ะฐ ะฒะธะบะพั€ะธัั‚ะพะฒัƒะนั‚ะต ะฝะพะฒัƒ ะฒะตั€ัั–ัŽ ะบะปั–ั”ะฝั‚ะฐ, ั‰ะพะฑ ะฟะตั€ะตะณะปัะฝัƒั‚ะธ ั†ัŽ ัั‚ะพั€ั–ะฝะบัƒ." enterListName: "ะ’ะฒะตะดั–ั‚ัŒ ะฝะฐะทะฒัƒ ัะฟะธัะบัƒ" privacy: "ะšะพะฝั„ั–ะดะตะฝั†ั–ะนะฝั–ัั‚ัŒ" makeFollowManuallyApprove: "ะŸั–ะดั‚ะฒะตั€ะดะถัƒะฒะฐั‚ะธ ะฟั–ะดะฟะธัะฝะธะบั–ะฒ ัƒั€ัƒั‡ะฝัƒ" @@ -103,6 +106,7 @@ clickToShow: "ะะฐั‚ะธัะฝั–ั‚ัŒ ะดะปั ะฟะตั€ะตะณะปัะดัƒ" sensitive: "NSFW" add: "ะ”ะพะดะฐั‚ะธ" reaction: "ะ ะตะฐะบั†ั–ั—" +reactionSetting: "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ั€ะตะฐะบั†ั–ะน" reactionSettingDescription2: "ะŸะตั€ะตะผั–ัั‚ะธั‚ะธ ั‰ะพะฑ ะทะผั–ะฝะธั‚ะธ ะฟะพั€ัะดะพะบ, ะšะปะฐั†ะฝัƒั‚ะธ ะผะธัˆะพัŽ ั‰ะพะฑ ะฒะธะดะฐะปะธั‚ะธ, ะะฐั‚ะธัะฝัƒั‚ะธ \"+\" ั‰ะพะฑ ะดะพะดะฐั‚ะธ." rememberNoteVisibility: "ะŸะฐะผโ€™ัั‚ะฐั‚ะธ ะฟะฐั€ะฐะผะตั‚ั€ะธ ะฒะธะดะธะผั–ัั‚ั–" attachCancel: "ะ’ะธะดะฐะปะธั‚ะธ ะฒะบะปะฐะดะตะฝะฝั" @@ -137,7 +141,10 @@ flagAsBot: "ะะบะฐัƒะฝั‚ ะฑะพั‚ะฐ" flagAsBotDescription: "ะ’ะฒั–ะผะบะฝั–ั‚ัŒ ัะบั‰ะพ ั†ะตะน ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะฒะธะบะพั€ะธัั‚ะพะฒัƒั”ั‚ัŒัั ะฑะพั‚ะพะผ. ะฆั ะพะฟั†ั–ั ะฟะพะทะฝะฐั‡ะธั‚ัŒ ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ัะบ ะฑะพั‚ะฐ. ะฆะต ะฟะพั‚ั€ั–ะฑะฝะพ ั‰ะพะฑ ะฒะธะบะปัŽั‡ะธั‚ะธ ะฑะตะทะบั–ะฝะตั‡ะฝัƒ ั–ะฝั‚ะตั€ะฐะบั†ั–ัŽ ะผั–ะถ ะฑะพั‚ะฐะผะธ ะฐ ั‚ะฐะบะพะถ ะฒั–ะดะฟะพะฒั–ะดะฝะพะณะพ ะฟั–ะดะปะฐัˆั‚ัƒะฒะฐะฝะฝั Misskey." flagAsCat: "ะะบะฐัƒะฝั‚ ะบะพั‚ะฐ" flagAsCatDescription: "ะ’ะฒั–ะผะบะฝั–ั‚ัŒ, ั‰ะพะฑ ะฟะพะทะฝะฐั‡ะธั‚ะธ, ั‰ะพ ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ั” ะบะพั‚ะธะบะพะผ." +flagShowTimelineReplies: "ะŸะพะบะฐะทัƒะฒะฐั‚ะธ ะฒั–ะดะฟะพะฒั–ะดั– ะฝะฐ ะฝะพั‚ะฐั‚ะบะธ ะฝะฐ ั‡ะฐัะพะฒั–ะน ัˆะบะฐะปั–" +flagShowTimelineRepliesDescription: "ะŸะพะบะฐะทัƒั” ะฒั–ะดะฟะพะฒั–ะดั– ะบะพั€ะธัั‚ัƒะฒะฐั‡ั–ะฒ ะฝะฐ ะฝะพั‚ะฐั‚ะบะธ ั–ะฝัˆะธั… ะบะพั€ะธัั‚ัƒะฒะฐั‡ั–ะฒ ะฝะฐ ั‡ะฐัะพะฒั–ะน ัˆะบะฐะปั–." autoAcceptFollowed: "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะฟั€ะธะนะผะฐั‚ะธ ะทะฐะฟะธั‚ะธ ะฝะฐ ะฟั–ะดะฟะธัะบัƒ ะฒั–ะด ะบะพั€ะธัั‚ัƒะฒะฐั‡ั–ะฒ, ะฝะฐ ัะบะธั… ะฒะธ ะฟั–ะดะฟะธัะฐะฝั–" +addAccount: "ะ”ะพะดะฐั‚ะธ ะฐะบะฐัƒะฝั‚" loginFailed: "ะะต ะฒะดะฐะปะพัั ัƒะฒั–ะนั‚ะธ" showOnRemote: "ะŸะตั€ะตะณะปัะฝัƒั‚ะธ ะฒ ะพั€ะธะณั–ะฝะฐะปั–" general: "ะ—ะฐะณะฐะปัŒะฝะต" @@ -148,6 +155,7 @@ searchWith: "ะŸะพัˆัƒะบ: {q}" youHaveNoLists: "ะฃ ะฒะฐั ะฝะตะผะฐั” ัะฟะธัะบั–ะฒ" followConfirm: "ะŸั–ะดะฟะธัะฐั‚ะธัั ะฝะฐ {name}?" proxyAccount: "ะŸั€ะพะบัั–-ะฐะบะฐัƒะฝั‚" +proxyAccountDescription: "ะžะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะฟั€ะพะบัั– โ€“ ั†ะต ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั, ัะบะธะน ะดั–ั” ัะบ ะฒั–ะดะดะฐะปะตะฝะธะน ะฟั–ะดะฟะธัะฝะธะบ ะดะปั ะบะพั€ะธัั‚ัƒะฒะฐั‡ั–ะฒ ะทะฐ ะฟะตะฒะฝะธั… ัƒะผะพะฒ. ะะฐะฟั€ะธะบะปะฐะด, ะบะพะปะธ ะบะพั€ะธัั‚ัƒะฒะฐั‡ ะดะพะดะฐั” ะฒั–ะดะดะฐะปะตะฝะพะณะพ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ ะดะพ ัะฟะธัะบัƒ, ะฐะบั‚ะธะฒะฝั–ัั‚ัŒ ะฒั–ะดะดะฐะปะตะฝะพะณะพ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ ะฝะต ะฑัƒะดะต ะดะพัั‚ะฐะฒะปะตะฝะฐ ะฝะฐ ัะตั€ะฒะตั€, ัะบั‰ะพ ะถะพะดะตะฝ ะปะพะบะฐะปัŒะฝะธะน ะบะพั€ะธัั‚ัƒะฒะฐั‡ ะฝะต ัั‚ะตะถะธั‚ัŒ ะทะฐ ั†ะธะผ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะตะผ, ั‚ะพ ะทะฐะผั–ัั‚ัŒ ะฝัŒะพะณะพ ะฑัƒะดะต ะฒะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธัั ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะฟั€ะพะบัั–-ัะตั€ะฒะตั€ะฐ." host: "ะฅะพัั‚" selectUser: "ะ’ะธะฑะตั€ั–ั‚ัŒ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ" recipient: "ะžั‚ั€ะธะผัƒะฒะฐั‡" @@ -229,6 +237,8 @@ resetAreYouSure: "ะกะฟั€ะฐะฒะดั– ัะบะธะฝัƒั‚ะธ?" saved: "ะ—ะฑะตั€ะตะถะตะฝะพ" messaging: "ะงะฐั‚ะธ" upload: "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ" +keepOriginalUploading: "ะ—ะฑะตั€ะตะณั‚ะธ ะพั€ะธะณั–ะฝะฐะปัŒะฝะต ะทะพะฑั€ะฐะถะตะฝะฝั" +keepOriginalUploadingDescription: "ะ—ะฑะตั€ั–ะณะฐั” ะฟะพั‡ะฐั‚ะบะพะฒะพ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะต ะทะพะฑั€ะฐะถะตะฝะฝั ัะบ ั”. ะฏะบั‰ะพ ะฒะธะผะบะฝะตะฝะพ, ะฒะตั€ัั–ั ะดะปั ะฒั–ะดะพะฑั€ะฐะถะตะฝะฝั ะฒ ะ†ะฝั‚ะตั€ะฝะตั‚ั– ะฑัƒะดะต ัั‚ะฒะพั€ะตะฝะฐ ะฟั–ะด ั‡ะฐั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั." fromDrive: "ะ— ะดะธัะบะฐ" fromUrl: "ะ— ะฟะพัะธะปะฐะฝะฝั" uploadFromUrl: "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ ะท ะฟะพัะธะปะฐะฝะฝั" @@ -275,6 +285,7 @@ emptyDrive: "ะ”ะธัะบ ะฟะพั€ะพะถะฝั–ะน" emptyFolder: "ะขะตะบะฐ ะฟะพั€ะพะถะฝั" unableToDelete: "ะ’ะธะดะฐะปะตะฝะฝั ะฝะตะผะพะถะปะธะฒะต" inputNewFileName: "ะ’ะฒะตะดั–ั‚ัŒ ั–ะผ'ั ะฝะพะฒะพะณะพ ั„ะฐะนะปัƒ" +inputNewDescription: "ะ’ะฒะตะดั–ั‚ัŒ ะฝะพะฒะธะน ะทะฐะณะพะปะพะฒะพะบ" inputNewFolderName: "ะ’ะฒะตะดั–ั‚ัŒ ั–ะผ'ั ะฝะพะฒะพั— ั‚ะตะบะธ" circularReferenceFolder: "ะ’ะธ ะฝะฐะผะฐะณะฐั”ั‚ะตััŒ ะฟะตั€ะตะผั–ัั‚ะธั‚ะธ ะฟะฐะฟะบัƒ ะฒ ั—ั— ะฟั–ะดะฟะฐะฟะบัƒ." hasChildFilesOrFolders: "ะฆั ั‚ะตะบะฐ ะฝะต ะฟะพั€ะพะถะฝั ั– ะฝะต ะผะพะถะต ะฑัƒั‚ะธ ะฒะธะดะฐะปะตะฝะฐ" @@ -306,6 +317,8 @@ monthX: "{month}" yearX: "{year}" pages: "ะกั‚ะพั€ั–ะฝะบะธ" integration: "ะ†ะฝั‚ะตะณั€ะฐั†ั–ั" +connectService: "ะŸั–ะดโ€™ั”ะดะฝะฐั‚ะธ" +disconnectService: "ะ’ั–ะดะบะปัŽั‡ะธั‚ะธัั" enableLocalTimeline: "ะฃะฒั–ะผะบะฝัƒั‚ะธ ะปะพะบะฐะปัŒะฝัƒ ัั‚ั€ั–ั‡ะบัƒ" enableGlobalTimeline: "ะฃะฒั–ะผะบะฝัƒั‚ะธ ะณะปะพะฑะฐะปัŒะฝัƒ ัั‚ั€ั–ั‡ะบัƒ" disablingTimelinesInfo: "ะะดะผั–ะฝั–ัั‚ั€ะฐั‚ะพั€ะธ ั‚ะฐ ะผะพะดะตั€ะฐั‚ะพั€ะธ ะทะฐะฒะถะดะธ ะผะฐัŽั‚ัŒ ะดะพัั‚ัƒะฟ ะดะพ ะฒัั–ั… ัั‚ั€ั–ั‡ะพะบ, ะฝะฐะฒั–ั‚ัŒ ัะบั‰ะพ ะฒะพะฝะธ ะฒะธะผะบะฝัƒั‚ั–." @@ -317,6 +330,7 @@ driveCapacityPerRemoteAccount: "ะžะฑ'ั”ะผ ะดะธัะบะฐ ะฝะฐ ะพะดะฝะพะณะพ ะฒั–ะดะด inMb: "ะ’ ะผะตะณะฐะฑะฐะนั‚ะฐั…" iconUrl: "URL ะฐะฒะฐั‚ะฐั€ะฐ" bannerUrl: "URL ะฑะฐะฝะตั€ะฐ" +backgroundImageUrl: "URL-ะฐะดั€ะตัะฐ ั„ะพะฝะพะฒะพะณะพ ะทะพะฑั€ะฐะถะตะฝะฝั" basicInfo: "ะžัะฝะพะฒะฝะฐ ั–ะฝั„ะพั€ะผะฐั†ั–ั" pinnedUsers: "ะ—ะฐะบั€ั–ะฟะปะตะฝั– ะบะพั€ะธัั‚ัƒะฒะฐั‡ั–" pinnedUsersDescription: "ะ’ะฟะธัˆั–ั‚ัŒ ะฒ ัะฟะธัะพะบ ะบะพั€ะธัั‚ัƒะฒะฐั‡ั–ะฒ, ัะบะธั… ั…ะพั‡ะตั‚ะต ะทะฐะบั€ั–ะฟะธั‚ะธ ะฝะฐ ัั‚ะพั€ั–ะฝั†ั– \"ะ—ะฝะฐะนั‚ะธ\", ั–ะผ'ั ะฒ ัั‚ะพะฒะฟั‡ะธะบ." @@ -332,6 +346,7 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "ะฃะฒั–ะผะบะฝัƒั‚ะธ reCAPTCHA" recaptchaSiteKey: "ะšะปัŽั‡ ัะฐะนั‚ัƒ" recaptchaSecretKey: "ะกะตะบั€ะตั‚ะฝะธะน ะบะปัŽั‡" +avoidMultiCaptchaConfirm: "ะ’ะธะบะพั€ะธัั‚ะฐะฝะฝั ะบั–ะปัŒะบะพั… ัะธัั‚ะตะผ Captcha ะผะพะถะต ัะฟั€ะธั‡ะธะฝะธั‚ะธ ะฟะตั€ะตัˆะบะพะดะธ ะผั–ะถ ะฝะธะผะธ. ะ‘ะฐะถะฐั”ั‚ะต ะฒะธะผะบะฝัƒั‚ะธ ั–ะฝัˆั– ะฐะบั‚ะธะฒะฝั– ัะธัั‚ะตะผะธ Captcha? ะฏะบั‰ะพ ะฒะธ ั…ะพั‡ะตั‚ะต, ั‰ะพะฑ ะฒะพะฝะธ ะทะฐะปะธัˆะฐะปะธัั ะฒะฒั–ะผะบะฝะตะฝะธะผะธ, ะฝะฐั‚ะธัะฝั–ั‚ัŒ ยซะกะบะฐััƒะฒะฐั‚ะธยป." antennas: "ะะฝั‚ะตะฝะธ" manageAntennas: "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ะฐะฝั‚ะตะฝ" name: "ะ†ะผ'ั" @@ -428,10 +443,12 @@ signinWith: "ะฃะฒั–ะนั‚ะธ ะทะฐ ะดะพะฟะพะผะพะณะพัŽ {x}" signinFailed: "ะะต ะฒะดะฐะปะพัั ัƒะฒั–ะนั‚ะธ. ะ’ะฒะตะดะตะฝั– ั–ะผโ€™ั ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ ะฐะฑะพ ะฟะฐั€ะพะปัŒ ะฝะตะฟั€ะฐะฒะธะปัŒะฝi." tapSecurityKey: "ะขะพั€ะบะฝั–ั‚ัŒัั ะบะปัŽั‡ะฐ ะฑะตะทะฟะตะบะธ" or: "ะฐะฑะพ" +language: "ะœะพะฒะฐ" uiLanguage: "ะœะพะฒะฐ ั–ะฝั‚ะตั€ั„ะตะนััƒ" groupInvited: "ะ—ะฐะฟั€ะพัˆะตะฝะฝั ะดะพ ะณั€ัƒะฟะธ" aboutX: "ะŸั€ะพ {x}" useOsNativeEmojis: "ะ’ะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ ะตะผะพะดะทั– ะžะก" +disableDrawer: "ะะต ะฒะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ ะฒะธััƒะฒะฝั– ะผะตะฝัŽ" youHaveNoGroups: "ะะตะผะฐั” ะณั€ัƒะฟ" joinOrCreateGroup: "ะžั‚ั€ะธะผัƒะนั‚ะต ะทะฐะฟั€ะพัˆะตะฝะฝั ะดะพ ะณั€ัƒะฟ ะฐะฑะพ ัั‚ะฒะพั€ัŽะนั‚ะต ัะฒะพั— ะฒะปะฐัะฝั– ะณั€ัƒะฟะธ." noHistory: "ะ†ัั‚ะพั€ั–ั ะฟะพั€ะพะถะฝั" @@ -442,6 +459,7 @@ category: "ะšะฐั‚ะตะณะพั€ั–ั" tags: "ะขะตะณะธ" docSource: "ะ”ะถะตั€ะตะปะพ ั†ัŒะพะณะพ ะดะพะบัƒะผะตะฝั‚ะฐ" createAccount: "ะกั‚ะฒะพั€ะธั‚ะธ ะฐะบะฐัƒะฝั‚" +existingAccount: "ะ†ัะฝัƒัŽั‡ะธะน ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั" regenerate: "ะžะฝะพะฒะธั‚ะธ" fontSize: "ะ ะพะทะผั–ั€ ัˆั€ะธั„ั‚ัƒ" noFollowRequests: "ะะตะผะฐั” ะทะฐะฟะธั‚ั–ะฒ ะฝะฐ ะฟั–ะดะฟะธัะบัƒ" @@ -463,6 +481,7 @@ showFeaturedNotesInTimeline: "ะŸะพะบะฐะทัƒะฒะฐั‚ะธ ะฟะพะฟัƒะปัั€ะฝั– ะฝะพั‚ะฐั‚ objectStorage: "Object Storage" useObjectStorage: "ะ’ะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ object storage" objectStorageBaseUrl: "Base URL" +objectStorageBaseUrlDesc: "ะฆะต ะฟะพั‡ะฐั‚ะบะพะฒะฐ ั‡ะฐัั‚ะธะฝะฐ ะฐะดั€ะตัะธ, ั‰ะพ ะฒะธะบะพั€ะธัั‚ะพะฒัƒั”ั‚ัŒัั CDN ะฐะฑะพ ะฟั€ะพะบัั–, ะฝะฐะฟั€ะธะบะปะฐะด ะดะปั S3: https://.s3.amazonaws.com, ะฐะฑะพ GCS: 'https://storage.googleapis.com/'" objectStorageBucket: "Bucket" objectStorageBucketDesc: "ะ‘ัƒะดัŒ ะปะฐัะบะฐ ะฒะบะฐะถั–ั‚ัŒ ะฝะฐะทะฒัƒ ะฒั–ะดั€ะฐ ะฒ ะฝะฐะปะฐัˆั‚ะพะฒะฐะฝะพะผัƒ ัะตั€ะฒั–ัั–." objectStoragePrefix: "Prefix" @@ -513,6 +532,9 @@ removeAllFollowing: "ะกะบะฐััƒะฒะฐั‚ะธ ะฒัั– ะฟั–ะดะฟะธัะบะธ" removeAllFollowingDescription: "ะกะบะฐััƒะฒะฐั‚ะธ ะฟั–ะดะฟะธัะบัƒ ะฝะฐ ะฒัั– ะฐะบะฐัƒะฝั‚ะธ ะท {host}. ะ‘ัƒะดัŒ ะปะฐัะบะฐ, ั€ะพะฑั–ั‚ัŒ ั†ะต, ัะบั‰ะพ ั–ะฝัั‚ะฐะฝั ะฑั–ะปัŒัˆะต ะฝะต ั–ัะฝัƒั”." userSuspended: "ะžะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะทะฐะฑะปะพะบะพะฒะฐะฝะธะน." userSilenced: "ะžะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะฟั€ะธะณะปัƒัˆะตะฝะธะน." +yourAccountSuspendedTitle: "ะฆะตะน ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะทะฐะฑะปะพะบะพะฒะฐะฝะพ" +yourAccountSuspendedDescription: "ะฆะตะน ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะฑัƒะปะพ ะทะฐะฑะปะพะบะพะฒะฐะฝะพ ั‡ะตั€ะตะท ะฟะพั€ัƒัˆะตะฝะฝั ัƒะผะพะฒ ะฝะฐะดะฐะฝะฝั ะฟะพัะปัƒะณ ัะตั€ะฒะตั€ะฐ. ะ—ะฒ'ัะถั–ั‚ัŒัั ะท ะฐะดะผั–ะฝั–ัั‚ั€ะฐั‚ะพั€ะพะผ, ัะบั‰ะพ ะฒะธ ั…ะพั‡ะตั‚ะต ะดั–ะทะฝะฐั‚ะธัั ะดะพะบะปะฐะดะฝั–ัˆัƒ ะฟั€ะธั‡ะธะฝัƒ. ะ‘ัƒะดัŒ ะปะฐัะบะฐ, ะฝะต ัั‚ะฒะพั€ัŽะนั‚ะต ะฝะพะฒะธะน ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั." +menu: "ะœะตะฝัŽ" divider: "ะ ะพะทะดั–ะปัŽะฒะฐั‡" addItem: "ะ”ะพะดะฐั‚ะธ ะตะปะตะผะตะฝั‚" relays: "ะ ะตั‚ั€ะฐะฝัะปัั‚ะพั€ะธ" @@ -531,6 +553,8 @@ disablePlayer: "ะ—ะฐะบั€ะธั‚ะธ ะฒั–ะดะตะพะฟะปะตั”ั€" expandTweet: "ะ ะพะทะณะพั€ะฝัƒั‚ะธ ั‚ะฒั–ั‚" themeEditor: "ะ ะตะดะฐะบั‚ะพั€ ั‚ะตะผ" description: "ะžะฟะธั" +describeFile: "ะ”ะพะดะฐั‚ะธ ะฟั–ะดะฟะธั" +enterFileDescription: "ะ’ะฒะตะดั–ั‚ัŒ ะฟั–ะดะฟะธั" author: "ะะฒั‚ะพั€" leaveConfirm: "ะ—ะผั–ะฝะธ ะฝะต ะทะฑะตั€ะตะถะตะฝั–. ะ’ะธ ะดั–ะนัะฝะพ ั…ะพั‡ะตั‚ะต ัะบะฐััƒะฒะฐั‚ะธ ะทะผั–ะฝะธ?" manage: "ะฃะฟั€ะฐะฒะปั–ะฝะฝั" @@ -553,6 +577,7 @@ pluginTokenRequestedDescription: "ะฆะตะน ะฟะปะฐะณั–ะฝ ะทะผะพะถะต ะฒะธะบะพั€ะธั notificationType: "ะขะธะฟ ัะฟะพะฒั–ั‰ะตะฝะฝั" edit: "ะ ะตะดะฐะณัƒะฒะฐั‚ะธ" useStarForReactionFallback: "ะ’ะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ โ˜… ัะบ ะทะฐะฟะฐัะฝะธะน ะฒะฐั€ั–ะฐะฝั‚, ัะบั‰ะพ ะตะผะพะดะทั– ั€ะตะฐะบั†ั–ั— ะฝะตะฒั–ะดะพะผะธะน" +emailServer: "ะกะตั€ะฒะตั€ ะตะปะตะบั‚ั€ะพะฝะฝะพั— ะฟะพัˆั‚ะธ" enableEmail: "ะฃะฒั–ะผะบะฝัƒั‚ะธ ั„ัƒะฝะบั†ั–ัŽ ะดะพัั‚ะฐะฒะบะธ ะฟะพัˆั‚ะธ" emailConfigInfo: "ะ’ะธะบะพั€ะธัั‚ะพะฒัƒั”ั‚ัŒัั ะดะปั ะฟั–ะดั‚ะฒะตั€ะดะถะตะฝะฝั ะตะปะตะบั‚ั€ะพะฝะฝะพั— ะฟะพัˆั‚ะธ ะฟั–ะดั‡ะฐั ั€ะตั”ัั‚ั€ะฐั†ั–ั—, ะฐ ั‚ะฐะบะพะถ ะดะปั ะฒั–ะดะฝะพะฒะปะตะฝะฝั ะฟะฐั€ะพะปัŽ." email: "E-mail" @@ -567,6 +592,9 @@ smtpSecure: "ะ’ะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ ะฑะตะทัƒะผะพะฒะฝะต ัˆะธั„ั€ัƒะฒะฐะฝ smtpSecureInfo: "ะ’ะธะผะบะฝั–ั‚ัŒ ะฟั€ะธ ะฒะธะบะพั€ะธัั‚ะฐะฝะฝั– STARTTLS " testEmail: "ะขะตัั‚ะพะฒะธะน email" wordMute: "ะ‘ะปะพะบัƒะฒะฐะฝะฝั ัะปั–ะฒ" +regexpError: "ะŸะพะผะธะปะบะฐ ั€ะตะณัƒะปัั€ะฝะพะณะพ ะฒะธั€ะฐะทัƒ" +regexpErrorDescription: "ะกั‚ะฐะปะฐัั ะฟะพะผะธะปะบะฐ ะฒ ั€ะตะณัƒะปัั€ะฝะพะผัƒ ะฒะธั€ะฐะทั– ะฒ ั€ัะดะบัƒ {line} ะฒะฐัˆะพะณะพ ัะปะพะฒะฐ {tab} ัะปะพะฒะฐ ั‰ะพ ั–ะณะฝะพั€ัƒัŽั‚ัŒัั:" +instanceMute: "ะŸั€ะธะณะปัƒัˆะตะฝะฝั ั–ะฝัั‚ะฐะฝัั–ะฒ" userSaysSomething: "{name} ั‰ะพััŒ ัะบะฐะทะฐะฒ(ะปะฐ)" makeActive: "ะะบั‚ะธะฒัƒะฒะฐั‚ะธ" display: "ะ’ั–ะดะพะฑั€ะฐะถะตะฝะฝั" @@ -594,6 +622,11 @@ reportAbuse: "ะŸะพัะบะฐั€ะถะธั‚ะธััŒ" reportAbuseOf: "ะŸะพัะบะฐั€ะถะธั‚ะธััŒ ะฝะฐ {name}" fillAbuseReportDescription: "ะ‘ัƒะดัŒ ะปะฐัะบะฐ ะฒะบะฐะถั–ั‚ัŒ ะฟะพะดั€ะพะฑะธั†ั– ัะบะฐั€ะณะธ. ะฏะบั‰ะพ ัะบะฐั€ะณะฐ ัั‚ะพััƒั”ั‚ัŒัั ะทะฐะฟะธััƒ, ะฒะบะฐะถั–ั‚ัŒ ะฟะพัะธะปะฐะฝะฝั ะฝะฐ ะฝัŒะพะณะพ." abuseReported: "ะ”ัะบัƒั”ะผะพ, ะฒะฐัˆัƒ ัะบะฐั€ะณัƒ ะฑัƒะปะพ ะฒั–ะดะฟั€ะฐะฒะปะตะฝะพ. " +reporter: "ะ ะตะฟะพั€ั‚ะตั€" +reporteeOrigin: "ะŸั€ะพ ะบะพะณะพ ะฟะพะฒั–ะดะพะผะปะตะฝะพ" +reporterOrigin: "ะฅั‚ะพ ะฟะพะฒั–ะดะพะผะธะฒ" +forwardReport: "ะŸะตั€ะตัะปะฐั‚ะธ ะทะฒั–ั‚ ะฝะฐ ะฒั–ะดะดะฐะปะตะฝะธะน ั–ะฝัั‚ะฐะฝั" +forwardReportIsAnonymous: "ะ—ะฐะผั–ัั‚ัŒ ะฒะฐัˆะพะณะพ ะพะฑะปั–ะบะพะฒะพะณะพ ะทะฐะฟะธััƒ ะฐะฝะพะฝั–ะผะฝะธะน ัะธัั‚ะตะผะฝะธะน ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั ะฑัƒะดะต ะฒั–ะดะพะฑั€ะฐะถะฐั‚ะธัั ัะบ ะดะพะฟะพะฒั–ะดะฐั‡ ะฝะฐ ะฒั–ะดะดะฐะปะตะฝะพะผัƒ ั–ะฝัั‚ะฐะฝัั–" send: "ะ’ั–ะดะฟั€ะฐะฒะธั‚ะธ" abuseMarkAsResolved: "ะŸะพะทะฝะฐั‡ะธั‚ะธ ัะบะฐั€ะณัƒ ัะบ ะฒะธั€ั–ัˆะตะฝัƒ" openInNewTab: "ะ’ั–ะดะบั€ะธั‚ะธ ะฒ ะฝะพะฒั–ะน ะฒะบะปะฐะดั†ั–" @@ -655,6 +688,7 @@ center: "ะฆะตะฝั‚ั€" wide: "ะจะธั€ะพะบะธะน" narrow: "ะ’ัƒะทัŒะบะธะน" reloadToApplySetting: "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ะฒะฒั–ะนะดะต ะฒ ะดั–ัŽ ะฟั€ะธ ะฟะตั€ะตะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั–. ะŸะตั€ะตะทะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ?" +needReloadToApply: "ะ—ะผั–ะฝะธ ะฝะฐะฑัƒะดัƒั‚ัŒ ั‡ะธะฝะฝะพัั‚ั– ะฟั–ัะปั ะฟะตั€ะตะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ัั‚ะพั€ั–ะฝะบะธ." showTitlebar: "ะŸะพะบะฐะทะฐั‚ะธ ั‚ะธั‚ัƒะปัŒะฝะธะน ั€ัะดะพะบ" clearCache: "ะžั‡ะธัั‚ะธั‚ะธ ะบะตัˆ" onlineUsersCount: "{n} ะบะพั€ะธัั‚ัƒะฒะฐั‡ั–ะฒ ะพะฝะปะฐะนะฝ" @@ -669,12 +703,28 @@ textColor: "ะขะตะบัั‚" saveAs: "ะ—ะฑะตั€ะตะณั‚ะธ ัะบโ€ฆ" advanced: "ะ ะพะทัˆะธั€ะตะฝั–" value: "ะ—ะฝะฐั‡ะตะฝะฝั" +createdAt: "ะกั‚ะฒะพั€ะตะฝะพ" updatedAt: "ะžัั‚ะฐะฝะฝั” ะพะฝะพะฒะปะตะฝะฝั" saveConfirm: "ะ—ะฑะตั€ะตะณั‚ะธ ะทะผั–ะฝะธ?" deleteConfirm: "ะ’ะธ ะดั–ะนัะฝะพ ะฑะฐะถะฐั”ั‚ะต ั†ะต ะฒะธะดะฐะปะธั‚ะธ?" invalidValue: "ะะตะบะพั€ะตะบั‚ะฝะต ะทะฝะฐั‡ะตะฝะฝั." registry: "ะ ะตั”ัั‚ั€" closeAccount: "ะ—ะฐะบั€ะธั‚ะธ ะพะฑะปั–ะบะพะฒะธะน ะทะฐะฟะธั" +currentVersion: "ะ’ะตั€ัั–ั, ั‰ะพ ะฒะธะบะพั€ะธัั‚ะพะฒัƒั”ั‚ัŒัั" +latestVersion: "ะกะฐะผะฐ ัะฒั–ะถะฐ ะฒะตั€ัั–ั" +youAreRunningUpToDateClient: "ะฃ ะฒะฐั ะฝะฐะนัะฒั–ะถั–ัˆะฐ ะฒะตั€ัั–ั ะบะปั–ั”ะฝั‚ะฐ." +newVersionOfClientAvailable: "ะ”ะพัั‚ัƒะฟะฝั–ัˆะฐ ัะฒั–ะถะฐ ะฒะตั€ัั–ั ะบะปั–ั”ะฝั‚ะฐ." +usageAmount: "ะ’ะธะบะพั€ะธัั‚ะฐะฝะต" +capacity: "ะ„ะผะฝั–ัั‚ัŒ" +inUse: "ะ—ะฐะนะฝัั‚ะพ" +editCode: "ะ ะตะดะฐะณัƒะฒะฐั‚ะธ ะฒะธั…ั–ะดะฝะธะน ั‚ะตะบัั‚" +apply: "ะ—ะฐัั‚ะพััƒะฒะฐั‚ะธ" +receiveAnnouncementFromInstance: "ะžั‚ั€ะธะผัƒะฒะฐั‚ะธ ะพะฟะพะฒั–ั‰ะตะฝะฝั ะท ั–ะฝัั‚ะฐะฝััƒ" +emailNotification: "ะกะฟะพะฒั–ั‰ะตะฝะฝั ะตะปะตะบั‚ั€ะพะฝะฝะพัŽ ะฟะพัˆั‚ะพัŽ" +publish: "ะžะฟัƒะฑะปั–ะบัƒะฒะฐั‚ะธ" +inChannelSearch: "ะŸะพัˆัƒะบ ะทะฐ ะบะฐะฝะฐะปะพะผ" +useReactionPickerForContextMenu: "ะ’ั–ะดะบั€ะธะฒะฐั‚ะธ ะฟะฐะปั–ั‚ั€ัƒ ั€ะตะฐะบั†ั–ะน ะฟั€ะฐะฒะพัŽ ะบะฝะพะฟะบะพัŽ" +typingUsers: "ะกั‚ัƒะบ ะบะปะฐะฒั–ัˆ. ะฆะต {users}โ€ฆ" goBack: "ะะฐะทะฐะด" info: "ะ†ะฝั„ะพั€ะผะฐั†ั–ั" user: "ะšะพั€ะธัั‚ัƒะฒะฐั‡ั–" @@ -687,6 +737,8 @@ hashtags: "ะฅะตัˆั‚ะตา‘" hide: "ะกั…ะพะฒะฐั‚ะธ" searchByGoogle: "ะŸะพัˆัƒะบ" indefinitely: "ะั–ะบะพะปะธ" +_ffVisibility: + public: "ะžะฟัƒะฑะปั–ะบัƒะฒะฐั‚ะธ" _ad: back: "ะะฐะทะฐะด" _gallery: @@ -867,7 +919,6 @@ _sfx: antenna: "ะŸั€ะธะนะพะผ ะฐะฝั‚ะตะฝะธ" channel: "ะŸะพะฒั–ะดะพะผะปะตะฝะฝั ะบะฐะฝะฐะปัƒ" _ago: - unknown: "ะะตะฒั–ะดะพะผะพ" future: "ะœะฐะนะฑัƒั‚ะฝั”" justNow: "ะฉะพะนะฝะพ" secondsAgo: "{n}ั ั‚ะพะผัƒ" @@ -1377,6 +1428,9 @@ _notification: followRequestAccepted: "ะŸั€ะธะนะฝัั‚ั– ะฟั–ะดะฟะธัะบะธ" groupInvited: "ะ—ะฐะฟั€ะพัˆะตะฝะฝั ะดะพ ะณั€ัƒะฟ" app: "ะกะฟะพะฒั–ั‰ะตะฝะฝั ะฒั–ะด ะดะพะดะฐั‚ะบั–ะฒ" + _actions: + reply: "ะ’ั–ะดะฟะพะฒั–ัั‚ะธ" + renote: "ะŸะพัˆะธั€ะธั‚ะธ" _deck: alwaysShowMainColumn: "ะ—ะฐะฒะถะดะธ ะฟะพะบะฐะทัƒะฒะฐั‚ะธ ะณะพะปะพะฒะฝัƒ ะบะพะปะพะฝะบัƒ" columnAlign: "ะ’ะธั€ั–ะฒะฝัั‚ะธ ัั‚ะพะฒะฟั†ั–" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 42f86b3359..9919e0a0a4 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1,35 +1,1093 @@ --- _lang_: "Tiแบฟng Viแป‡t" headlineMisskey: "Mแบกng xรฃ hแป™i liรชn hแปฃp" +introMisskey: "Xin chร o! Misskey lร  mแป™t nแปn tแบฃng tiแปƒu blog phi tแบญp trung mรฃ nguแป“n mแปŸ.\nViแบฟt \"tรบt\" ฤ‘แปƒ chia sแบป nhแปฏng suy nghฤฉ cแปงa bแบกn ๐Ÿ“ก\nBแบฑng \"biแปƒu cแบฃm\", bแบกn cรณ thแปƒ bร y tแป nhanh chรณng cแบฃm xรบc cแปงa bแบกn vแป›i cรกc tรบt ๐Ÿ‘\nHรฃy khรกm phรก mแป™t thแบฟ giแป›i mแป›i! ๐Ÿš€" monthAndDay: "{day} thรกng {month}" search: "Tรฌm kiแบฟm" notifications: "Thรดng bรกo" username: "Tรชn ngฦฐแปi dรนng" password: "Mแบญt khแบฉu" forgotPassword: "Quรชn mแบญt khแบฉu" +fetchingAsApObject: "ฤang nแบกp dแปฏ liแป‡u tแปซ Fediverse..." ok: "ฤแป“ng รฝ" +gotIt: "ฤรฃ hiแปƒu!" +cancel: "Hแปงy" +enterUsername: "Nhแบญp tรชn ngฦฐแปi dรนng" renotedBy: "Chia sแบป bแปŸi {user}" +noNotes: "Chฦฐa cรณ tรบt nร o." +noNotifications: "Khรดng cรณ thรดng bรกo" +instance: "Mรกy chแปง" +settings: "Cร i ฤ‘แบทt" +basicSettings: "Thiแบฟt lแบญp chung" +otherSettings: "Thiแบฟt lแบญp khรกc" +openInWindow: "MแปŸ trong cแปญa sแป• mแป›i" +profile: "Trang cรก nhรขn" +timeline: "Bแบฃng tin" +noAccountDescription: "Ngฦฐแปi nร y chฦฐa viแบฟt mรด tแบฃ." +login: "ฤฤƒng nhแบญp" +loggingIn: "ฤang ฤ‘ฤƒng nhแบญp..." +logout: "ฤฤƒng xuแบฅt" +signup: "ฤฤƒng kรฝ" +uploading: "ฤang tแบฃi lรชnโ€ฆ" +save: "Lฦฐu" +users: "Ngฦฐแปi dรนng" +addUser: "Thรชm ngฦฐแปi dรนng" +favorite: "Thรชm vร o yรชu thรญch" +favorites: "Lฦฐแปฃt thรญch" +unfavorite: "Bแป thรญch" +favorited: "ฤรฃ thรชm vร o yรชu thรญch." +alreadyFavorited: "ฤรฃ thรชm vร o yรชu thรญch rแป“i." +cantFavorite: "Khรดng thแปƒ thรชm vร o yรชu thรญch." +pin: "Ghim" +unpin: "Bแป ghim" +copyContent: "Chรฉp nแป™i dung" +copyLink: "Chรฉp liรชn kแบฟt" +delete: "Xรณa" +deleteAndEdit: "Sแปญa" +deleteAndEditConfirm: "Bแบกn cรณ chแบฏc muแป‘n sแปญa tรบt nร y? Nhแปฏng biแปƒu cแบฃm, lฦฐแปฃt trแบฃ lแปi vร  ฤ‘ฤƒng lแบกi sแบฝ bแป‹ mแบฅt." +addToList: "Thรชm vร o danh sรกch" +sendMessage: "Gแปญi tin nhแบฏn" +copyUsername: "Chรฉp tรชn ngฦฐแปi dรนng" +searchUser: "Tรฌm kiแบฟm ngฦฐแปi dรนng" +reply: "Trแบฃ lแปi" +loadMore: "Tแบฃi thรชm" +showMore: "Xem thรชm" +youGotNewFollower: "ฤ‘รฃ theo dรตi bแบกn" +receiveFollowRequest: "ฤรฃ yรชu cแบงu theo dรตi" +followRequestAccepted: "ฤรฃ chแบฅp nhแบญn yรชu cแบงu theo dรตi" +mention: "Nhแบฏc ฤ‘แบฟn" +mentions: "Lฦฐแปฃt nhแบฏc" +directNotes: "Nhแบฏn riรชng" +importAndExport: "Nhแบญp vร  xuแบฅt dแปฏ liแป‡u" +import: "Nhแบญp dแปฏ liแป‡u" +export: "Xuแบฅt dแปฏ liแป‡u" +files: "Tแบญp tin" +download: "Tแบฃi xuแป‘ng" +driveFileDeleteConfirm: "Bแบกn cรณ chแบฏc muแป‘n xรณa tแบญp tin \"{name}\"? Tรบt liรชn quan cลฉng sแบฝ bแป‹ xรณa theo." +unfollowConfirm: "Bแบกn cรณ chแบฏc muแป‘n ngฦฐng theo dรตi {name}?" +exportRequested: "ฤang chuแบฉn bแป‹ xuแบฅt tแบญp tin. Quรก trรฌnh nร y cรณ thแปƒ mแบฅt รญt phรบt. Nรณ sแบฝ ฤ‘ฦฐแปฃc tแปฑ ฤ‘แป™ng thรชm vร o Drive sau khi hoร n thร nh." +importRequested: "Bแบกn vแปซa yรชu cแบงu nhแบญp dแปฏ liแป‡u. Quรก trรฌnh nร y cรณ thแปƒ mแบฅt รญt phรบt." +lists: "Danh sรกch" +noLists: "Bแบกn chฦฐa cรณ danh sรกch nร o" +note: "Tรบt" +notes: "Tรบt" +following: "ฤang theo dรตi" +followers: "Ngฦฐแปi theo dรตi" +followsYou: "Theo dรตi bแบกn" +createList: "Tแบกo danh sรกch" +manageLists: "Quแบฃn lรฝ danh sรกch" +error: "Lแป—i" +somethingHappened: "Xแบฃy ra lแป—i" +retry: "Thแปญ lแบกi" +pageLoadError: "Xแบฃy ra lแป—i khi tแบฃi trang." +pageLoadErrorDescription: "Cรณ thแปƒ lร  do bแป™ nhแป› ฤ‘แป‡m cแปงa trรฌnh duyแป‡t. Hรฃy thแปญ xรณa bแป™ nhแป› ฤ‘แป‡m vร  thแปญ lแบกi sau รญt phรบt." +serverIsDead: "Mรกy chแปง khรดng phแบฃn hแป“i. Vui lรฒng thแปญ lแบกi sau giรขy lรกt." +youShouldUpgradeClient: "ฤแปƒ xem trang nร y, hรฃy lร m tฦฐฦกi ฤ‘แปƒ cแบญp nhแบญt แปฉng dแปฅng." +enterListName: "ฤแบทt tรชn cho danh sรกch" +privacy: "Bแบฃo mแบญt" +makeFollowManuallyApprove: "Yรชu cแบงu theo dรตi cแบงn ฤ‘ฦฐแปฃc duyแป‡t" +defaultNoteVisibility: "Kiแปƒu tรบt mแบทc ฤ‘แป‹nh" +follow: "ฤang theo dรตi" +followRequest: "Gแปญi yรชu cแบงu theo dรตi" +followRequests: "Yรชu cแบงu theo dรตi" +unfollow: "Ngฦฐng theo dรตi" +followRequestPending: "Yรชu cแบงu theo dรตi ฤ‘ang chแป" +enterEmoji: "Chรจn emoji" +renote: "ฤฤƒng lแบกi" +unrenote: "Hแปงy ฤ‘ฤƒng lแบกi" +renoted: "ฤรฃ ฤ‘ฤƒng lแบกi." +cantRenote: "Khรดng thแปƒ ฤ‘ฤƒng lแบกi tรบt nร y." +cantReRenote: "Khรดng thแปƒ ฤ‘ฤƒng lแบกi mแป™t tรบt ฤ‘ฤƒng lแบกi." +quote: "Trรญch dแบซn" +pinnedNote: "Tรบt ghim" +pinned: "Ghim" +you: "Bแบกn" +clickToShow: "Nhแบฅn ฤ‘แปƒ xem" +sensitive: "Nhแบกy cแบฃm" +add: "Thรชm" +reaction: "Biแปƒu cแบฃm" +reactionSetting: "Chแปn nhแปฏng biแปƒu cแบฃm hiแปƒn thแป‹" +reactionSettingDescription2: "Kรฉo ฤ‘แปƒ sแบฏp xแบฟp, nhแบฅn ฤ‘แปƒ xรณa, nhแบฅn \"+\" ฤ‘แปƒ thรชm." +rememberNoteVisibility: "Lฦฐu kiแปƒu tรบt mแบทc ฤ‘แป‹nh" +attachCancel: "Gแปก tแบญp tin ฤ‘รญnh kรจm" +markAsSensitive: "ฤรกnh dแบฅu lร  nhแบกy cแบฃm" +unmarkAsSensitive: "Bแป ฤ‘รกnh dแบฅu nhแบกy cแบฃm" +enterFileName: "Nhแบญp tรชn tแบญp tin" +mute: "แบจn" +unmute: "Bแป แบฉn" +block: "Chแบทn" +unblock: "Bแป chแบทn" +suspend: "Vรด hiแป‡u hรณa" +unsuspend: "Bแป vรด hiแป‡u hรณa" +blockConfirm: "Bแบกn cรณ chแบฏc muแป‘n chแบทn ngฦฐแปi nร y?" +unblockConfirm: "Bแบกn cรณ chแบฏc muแป‘n bแป chแบทn ngฦฐแปi nร y?" +suspendConfirm: "Bแบกn cรณ chแบฏc muแป‘n vรด hiแป‡u hรณa ngฦฐแปi nร y?" +unsuspendConfirm: "Bแบกn cรณ chแบฏc muแป‘n bแป vรด hiแป‡u hรณa ngฦฐแปi nร y?" +selectList: "Chแปn danh sรกch" +selectAntenna: "Chแปn mแป™t antenna" +selectWidget: "Chแปn tiแป‡n รญch" +editWidgets: "Sแปญa tiแป‡n รญch" +editWidgetsExit: "Xong" +customEmojis: "Tรนy chแป‰nh emoji" +emoji: "Emoji" +emojis: "Emoji" +emojiName: "Tรชn emoji" +emojiUrl: "URL Emoji" +addEmoji: "Thรชm emoji" +settingGuide: "Cร i ฤ‘แบทt ฤ‘แป xuแบฅt" +cacheRemoteFiles: "Tแบญp tin cache tแปซ xa" +cacheRemoteFilesDescription: "Khi tรนy chแปn nร y bแป‹ tแบฏt, cรกc tแบญp tin tแปซ xa sแบฝ ฤ‘ฦฐแปฃc tแบฃi trแปฑc tiแบฟp tแปซ mรกy chแปง khรกc. ฤiแปu nร y sแบฝ giรบp giแบฃm dung lฦฐแปฃng lฦฐu trแปฏ nhฦฐng lแบกi tฤƒng lฦฐu lฦฐแปฃng truy cแบญp, vรฌ hรฌnh thu nhแป sแบฝ khรดng ฤ‘ฦฐแปฃc tแบกo." flagAsBot: "ฤรกnh dแบฅu ฤ‘รขy lร  tร i khoแบฃn bot" +flagAsBotDescription: "Bแบญt tรนy chแปn nร y nแบฟu tร i khoแบฃn nร y ฤ‘ฦฐแปฃc kiแปƒm soรกt bแปŸi mแป™t chฦฐฦกng trรฌnh. Nแบฟu ฤ‘ฦฐแปฃc bแบญt, nรณ sแบฝ ฤ‘ฦฐแปฃc ฤ‘รกnh dแบฅu ฤ‘แปƒ cรกc nhร  phรกt triแปƒn khรกc ngฤƒn chแบทn chuแป—i tฦฐฦกng tรกc vรด tแบญn vแป›i cรกc bot khรกc vร  ฤ‘iแปu chแป‰nh hแป‡ thแป‘ng nแป™i bแป™ cแปงa Misskey ฤ‘แปƒ coi tร i khoแบฃn nร y nhฦฐ mแป™t bot." +flagAsCat: "Tร i khoแบฃn nร y lร  mรจo" +flagAsCatDescription: "Bแบญt tรนy chแปn nร y ฤ‘แปƒ ฤ‘รกnh dแบฅu tร i khoแบฃn lร  mแป™t con mรจo." +flagShowTimelineReplies: "Hiแป‡n lฦฐแปฃt trแบฃ lแปi trong bแบฃng tin" +flagShowTimelineRepliesDescription: "Hiแป‡n lฦฐแปฃt trแบฃ lแปi cแปงa ngฦฐแปi bแบกn theo dรตi trรชn tรบt cแปงa nhแปฏng ngฦฐแปi khรกc." +autoAcceptFollowed: "Tแปฑ ฤ‘แป™ng phรช duyแป‡t theo dรตi tแปซ nhแปฏng ngฦฐแปi mร  bแบกn ฤ‘ang theo dรตi" +addAccount: "Thรชm tร i khoแบฃn" +loginFailed: "ฤฤƒng nhแบญp khรดng thร nh cรดng" +showOnRemote: "Truy cแบญp trang cแปงa ngฦฐแปi nร y" +general: "Tแป•ng quan" +wallpaper: "แบขnh bรฌa" +setWallpaper: "ฤแบทt แบฃnh bรฌa" +removeWallpaper: "Xรณa แบฃnh bรฌa" searchWith: "Tรฌm kiแบฟm: {q}" +youHaveNoLists: "Bแบกn chฦฐa cรณ danh sรกch nร o" followConfirm: "Bแบกn cรณ chแบฏc muแป‘n theo dรตi {name}๏ผŸ" +proxyAccount: "Tร i khoแบฃn proxy" +proxyAccountDescription: "Tร i khoแบฃn proxy lร  tร i khoแบฃn hoแบกt ฤ‘แป™ng nhฦฐ mแป™t ngฦฐแปi theo dรตi tแปซ xa cho ngฦฐแปi dรนng trong nhแปฏng ฤ‘iแปu kiแป‡n nhแบฅt ฤ‘แป‹nh. Vรญ dแปฅ: khi ngฦฐแปi dรนng thรชm ngฦฐแปi dรนng tแปซ xa vร o danh sรกch, hoแบกt ฤ‘แป™ng cแปงa ngฦฐแปi dรนng tแปซ xa sแบฝ khรดng ฤ‘ฦฐแปฃc chuyแปƒn ฤ‘แบฟn phiรชn bแบฃn nแบฟu khรดng cรณ ngฦฐแปi dรนng cแปฅc bแป™ nร o theo dรตi ngฦฐแปi dรนng ฤ‘รณ, vรฌ vแบญy tร i khoแบฃn proxy sแบฝ theo dรตi." +host: "Host" +selectUser: "Chแปn ngฦฐแปi dรนng" +recipient: "Ngฦฐแปi nhแบญn" +annotation: "Bรฌnh luแบญn" +federation: "Liรชn hแปฃp" +instances: "Mรกy chแปง" +registeredAt: "ฤฤƒng kรฝ vร o" +latestRequestSentAt: "Yรชu cแบงu cuแป‘i gแปญi lรบc" +latestRequestReceivedAt: "Yรชu cแบงu cuแป‘i nhแบญn lรบc" +latestStatus: "Trแบกng thรกi cuแป‘i cรนng" +storageUsage: "Dung lฦฐแปฃng lฦฐu trแปฏ" +charts: "ฤแป“ thแป‹" +perHour: "Mแป—i Giแป" +perDay: "Mแป—i Ngร y" +stopActivityDelivery: "Ngฦฐng gแปญi hoแบกt ฤ‘แป™ng" +blockThisInstance: "Chแบทn mรกy chแปง nร y" +operations: "Vแบญn hร nh" +software: "Phแบงn mแปm" +version: "Phiรชn bแบฃn" +metadata: "Metadata" +withNFiles: "{n} tแบญp tin" +monitor: "Giรกm sรกt" +jobQueue: "Cรดng viแป‡c chแป xแปญ lรฝ" cpuAndMemory: "CPU vร  Dung lฦฐแปฃng" +network: "Mแบกng" +disk: "แป” ฤ‘ฤฉa" +instanceInfo: "Thรดng tin mรกy chแปง" +statistics: "Thแป‘ng kรช" +clearQueue: "Xรณa hร ng ฤ‘แปฃi" +clearQueueConfirmTitle: "Bแบกn cรณ chแบฏc muแป‘n xรณa hร ng ฤ‘แปฃi?" +clearQueueConfirmText: "Mแปi tรบt chฦฐa ฤ‘ฦฐแปฃc gแปญi cรฒn lแบกi trong hร ng ฤ‘แปฃi sแบฝ khรดng ฤ‘ฦฐแปฃc liรชn hแปฃp. Thรดng thฦฐแปng thao tรกc nร y khรดng cแบงn thiแบฟt." +clearCachedFiles: "Xรณa bแป™ nhแป› ฤ‘แป‡m" +clearCachedFilesConfirm: "Bแบกn cรณ chแบฏc muแป‘n xรณa sแบกch bแป™ nhแป› ฤ‘แป‡m?" +blockedInstances: "Mรกy chแปง ฤ‘รฃ chแบทn" +blockedInstancesDescription: "Danh sรกch nhแปฏng mรกy chแปง bแบกn muแป‘n chแบทn. Chรบng sแบฝ khรดng thแปƒ giao tiแบฟp vแป›i mรกy chแปงy nร y nแปฏa." +muteAndBlock: "แบจn vร  Chแบทn" +mutedUsers: "Ngฦฐแปi ฤ‘รฃ แบฉn" +blockedUsers: "Ngฦฐแปi ฤ‘รฃ chแบทn" +noUsers: "Chฦฐa cรณ ai" +editProfile: "Sแปญa hแป“ sฦก" +noteDeleteConfirm: "Bแบกn cรณ chแบฏc muแป‘n xรณa tรบt nร y?" +pinLimitExceeded: "Bแบกn ฤ‘รฃ ฤ‘แบกt giแป›i hแบกn sแป‘ lฦฐแปฃng tรบt cรณ thแปƒ ghim" +intro: "ฤรฃ cร i ฤ‘แบทt Misskey! Xin hรฃy tแบกo tร i khoแบฃn admin." +done: "Xong" +processing: "ฤang xแปญ lรฝ" +preview: "Xem trฦฐแป›c" +default: "Mแบทc ฤ‘แป‹nh" +noCustomEmojis: "Khรดng cรณ emoji" +noJobs: "Khรดng cรณ cรดng viแป‡c" +federating: "ฤang liรชn hแปฃp" +blocked: "ฤรฃ chแบทn" +suspended: "ฤรฃ vรด hiแป‡u hรณa" +all: "Tแบฅt cแบฃ" +subscribing: "ฤang ฤ‘ฤƒng kรฝ" +publishing: "ฤang ฤ‘ฤƒng" +notResponding: "Khรดng cรณ phแบฃn hแป“i" +instanceFollowing: "ฤang theo dรตi mรกy chแปง" +instanceFollowers: "Ngฦฐแปi theo dรตi cแปงa mรกy chแปง" +instanceUsers: "Ngฦฐแปi dรนng trรชn mรกy chแปง nร y" +changePassword: "ฤแป•i mแบญt khแบฉu" +security: "Bแบฃo mแบญt" +retypedNotMatch: "Mแบญt khแบฉu khรดng trรนng khแป›p." +currentPassword: "Mแบญt khแบฉu hiแป‡n tแบกi" +newPassword: "Mแบญt khแบฉu mแป›i" +newPasswordRetype: "Nhแบญp lแบกi mแบญt khแบฉu mแป›i" +attachFile: "ฤรญnh kรจm tแบญp tin" +more: "Thรชm nแปฏa!" +featured: "Nแป•i bแบญt" +usernameOrUserId: "Tรชn ngฦฐแปi dรนng hoแบทc ID" +noSuchUser: "Khรดng tรฌm thแบฅy ngฦฐแปi dรนng" +lookup: "Tรฌm kiแบฟm" +announcements: "Thรดng bรกo" +imageUrl: "URL แบฃnh" +remove: "Xรณa" +removed: "ฤรฃ xรณa" +removeAreYouSure: "Bแบกn cรณ chแบฏc muแป‘n gแปก \"{x}\"?" +deleteAreYouSure: "Bแบกn cรณ chแบฏc muแป‘n xรณa \"{x}\"?" +resetAreYouSure: "Bแบกn cรณ chแบฏc muแป‘n ฤ‘แบทt lแบกi?" +saved: "ฤรฃ lฦฐu" +messaging: "Trรฒ chuyแป‡n" +upload: "Tแบฃi lรชn" +keepOriginalUploading: "Giแปฏ hรฌnh แบฃnh gแป‘c" +keepOriginalUploadingDescription: "Giแปฏ nguyรชn nhฦฐ hรฌnh แบฃnh ฤ‘ฦฐแปฃc tแบฃi lรชn ban ฤ‘แบงu. Nแบฟu tแบฏt, mแป™t phiรชn bแบฃn ฤ‘แปƒ hiแปƒn thแป‹ trรชn web sแบฝ ฤ‘ฦฐแปฃc tแบกo khi tแบฃi lรชn." +fromDrive: "Tแปซ แป• ฤ‘ฤฉa" +fromUrl: "Tแปซ URL" +uploadFromUrl: "Tแบฃi lรชn bแบฑng mแป™t URL" +uploadFromUrlDescription: "URL cแปงa tแบญp tin bแบกn muแป‘n tแบฃi lรชn" +uploadFromUrlRequested: "ฤรฃ yรชu cแบงu tแบฃi lรชn" +uploadFromUrlMayTakeTime: "Sแบฝ mแบฅt mแป™t khoแบฃng thแปi gian ฤ‘แปƒ tแบฃi lรชn xong." +explore: "Khรกm phรก" +messageRead: "ฤรฃ ฤ‘แปc" +noMoreHistory: "Khรดng cรฒn gรฌ ฤ‘แปƒ ฤ‘แปc" +startMessaging: "Bแบฏt ฤ‘แบงu trรฒ chuyแป‡n" +nUsersRead: "ฤ‘แปc bแปŸi {n}" +agreeTo: "Tรดi ฤ‘แป“ng รฝ {0}" +tos: "ฤiแปu khoแบฃn dแป‹ch vแปฅ" +start: "Bแบฏt ฤ‘แบงu" +home: "Trang chรญnh" +remoteUserCaution: "Vรฌ ngฦฐแปi dรนng nร y แปŸ mรกy chแปง khรกc, thรดng tin hiแปƒn thแป‹ cรณ thแปƒ khรดng ฤ‘แบงy ฤ‘แปง." +activity: "Hoแบกt ฤ‘แป™ng" +images: "Hรฌnh แบฃnh" +birthday: "Sinh nhรขฬฃt" +yearsOld: "{age} tuแป•i" +registeredDate: "Tham gia" +location: "ฤแบฟn tแปซ" +theme: "Chแปง ฤ‘แป" +themeForLightMode: "Chแปง ฤ‘แป dรนng trong trong chแบฟ ฤ‘แป™ Sรกng" +themeForDarkMode: "Chแปง ฤ‘แป dรนng trong chแบฟ ฤ‘แป™ Tแป‘i" +light: "Sรกng" +dark: "Tแป‘i" +lightThemes: "Nhแปฏng chแปง ฤ‘แป sรกng" +darkThemes: "Nhแปฏng chแปง ฤ‘แป tแป‘i" +syncDeviceDarkMode: "ฤแป“ng bแป™ vแป›i thiแบฟt bแป‹" +drive: "แป” ฤ‘ฤฉa" +fileName: "Tรชn tแบญp tin" +selectFile: "Chแปn tแบญp tin" +selectFiles: "Chแปn nhiแปu tแบญp tin" +selectFolder: "Chแปn thฦฐ mแปฅc" +selectFolders: "Chแปn nhiแปu thฦฐ mแปฅc" +renameFile: "ฤแป•i tรชn tแบญp tin" +folderName: "Tรชn thฦฐ mแปฅc" +createFolder: "Tแบกo thฦฐ mแปฅc" +renameFolder: "ฤแป•i tรชn thฦฐ mแปฅc" +deleteFolder: "Xรณa thฦฐ mแปฅc" +addFile: "Thรชm tแบญp tin" +emptyDrive: "แป” ฤ‘ฤฉa cแปงa bแบกn trแป‘ng trฦกn" +emptyFolder: "Thฦฐ mแปฅc trแป‘ng" +unableToDelete: "Khรดng thแปƒ xรณa" +inputNewFileName: "Nhแบญp tรชn mแป›i cho tแบญp tin" +inputNewDescription: "Nhแบญp mรด tแบฃ mแป›i" +inputNewFolderName: "Nhแบญp tรชn mแป›i cho thฦฐ mแปฅc" +circularReferenceFolder: "Thฦฐ mแปฅc ฤ‘รญch lร  mแป™t thฦฐ mแปฅc con cแปงa thฦฐ mแปฅc bแบกn muแป‘n di chuyแปƒn." +hasChildFilesOrFolders: "Khรดng thแปƒ xรณa cho ฤ‘แบฟn khi khรดng cรฒn gรฌ trong thฦฐ mแปฅc." +copyUrl: "Sao chรฉp URL" +rename: "ฤแป•i tรชn" +avatar: "แบขnh ฤ‘แบกi diแป‡n" +banner: "แบขnh bรฌa" +nsfw: "Nhแบกy cแบฃm" +whenServerDisconnected: "Khi mแบฅt kแบฟt nแป‘i tแป›i mรกy chแปง" +disconnectedFromServer: "Mแบฅt kแบฟt nแป‘i tแป›i mรกy chแปง" +reload: "Tแบฃi lแบกi" +doNothing: "Bแป qua" +reloadConfirm: "Bแบกn cรณ muแป‘n thแปญ tแบฃi lแบกi bแบฃng tin?" +watch: "Xem" +unwatch: "Ngแปซng xem" +accept: "ฤแป“ng รฝ" +reject: "Tแปซ chแป‘i" +normal: "Bรฌnh thฦฐแปng" +instanceName: "Tรชn mรกy chแปง" +instanceDescription: "Mรด tแบฃ mรกy chแปง" +maintainerName: "ฤแป™i ngลฉ vแบญn hร nh" +maintainerEmail: "Email ฤ‘แป™i ngลฉ" +tosUrl: "URL ฤiแปu khoแบฃn dแป‹ch vแปฅ" +thisYear: "Nฤƒm" +thisMonth: "Thรกng" +today: "Hรดm nay" dayX: "{day}" +monthX: "{month}" yearX: "{year}" +pages: "Trang" +integration: "Tฦฐฦกng tรกc" +connectService: "Kแบฟt nแป‘i" +disconnectService: "Ngแบฏt kแบฟt nแป‘i" +enableLocalTimeline: "Bแบญt bแบฃng tin mรกy chแปง" +enableGlobalTimeline: "Bแบญt bแบฃng tin liรชn hแปฃp" +disablingTimelinesInfo: "Quแบฃn trแป‹ viรชn vร  Kiแปƒm duyแป‡t viรชn luรดn cรณ quyแปn truy cแบญp mแปi bแบฃng tin, kแปƒ cแบฃ khi chรบng khรดng ฤ‘ฦฐแปฃc bแบญt." +registration: "ฤฤƒng kรฝ" +enableRegistration: "Cho phรฉp ฤ‘ฤƒng kรฝ mแป›i" +invite: "Mแปi" +driveCapacityPerLocalAccount: "Dung lฦฐแปฃng แป• ฤ‘ฤฉa tแป‘i ฤ‘a cho mแป—i ngฦฐแปi dรนng" +driveCapacityPerRemoteAccount: "Dung lฦฐแปฃng แป• ฤ‘ฤฉa tแป‘i ฤ‘a cho mแป—i ngฦฐแปi dรนng tแปซ xa" +inMb: "Tรญnh bแบฑng MB" +iconUrl: "URL Icon" +bannerUrl: "URL แบขnh bรฌa" +backgroundImageUrl: "URL แบขnh nแปn" +basicInfo: "Thรดng tin cฦก bแบฃn" +pinnedUsers: "Nhแปฏng ngฦฐแปi thรบ vแป‹" +pinnedUsersDescription: "Liแป‡t kรช mแป—i hร ng mแป™t tรชn ngฦฐแปi dรนng xuแป‘ng dรฒng ฤ‘แปƒ ghim trรชn tab \"Khรกm phรก\"." +pinnedPages: "Trang ฤ‘รฃ ghim" +pinnedPagesDescription: "Liแป‡t kรช cรกc trang thรบ vแป‹ ฤ‘แปƒ ghim trรชn mรกy chแปง." +pinnedClipId: "ID cแปงa clip muแป‘n ghim" +pinnedNotes: "Tรบt ghim" +hcaptcha: "hCaptcha" +enableHcaptcha: "Bแบญt hCaptcha" +hcaptchaSiteKey: "Khรณa cแปงa trang" +hcaptchaSecretKey: "Khรณa bรญ mแบญt" +recaptcha: "reCAPTCHA" +enableRecaptcha: "Bแบญt reCAPTCHA" +recaptchaSiteKey: "Khรณa cแปงa trang" +recaptchaSecretKey: "Khรณa bรญ mแบญt" +avoidMultiCaptchaConfirm: "Dรนng nhiแปu hแป‡ thแป‘ng Captcha cรณ thแปƒ gรขy nhiแป…u giแปฏa chรบng. Bแบกn cรณ muแป‘n tแบฏt cรกc hแป‡ thแป‘ng Captcha khรกc hiแป‡n ฤ‘ang hoแบกt ฤ‘แป™ng khรดng? Nแบฟu bแบกn muแป‘n chรบng tiแบฟp tแปฅc ฤ‘ฦฐแปฃc bแบญt, hรฃy nhแบฅn hแปงy." +antennas: "Trแบกm phรกt sรณng" +manageAntennas: "Quแบฃn lรฝ trแบกm phรกt sรณng" +name: "Tรชn" +antennaSource: "Nguแป“n trแบกm phรกt sรณng" +antennaKeywords: "Tแปซ khรณa ฤ‘แปƒ nghe" +antennaExcludeKeywords: "Tแปซ khรณa ฤ‘แปƒ lแปc ra" +antennaKeywordsDescription: "Phรขn cรกch bแบฑng dแบฅu cรกch cho ฤ‘iแปu kiแป‡n AND hoแบทc bแบฑng xuแป‘ng dรฒng cho ฤ‘iแปu kiแป‡n OR." +notifyAntenna: "Thรดng bรกo cรณ tรบt mแป›i" +withFileAntenna: "Chแป‰ nhแปฏng tรบt cรณ media" +enableServiceworker: "Bแบญt ServiceWorker" +antennaUsersDescription: "Liแป‡t kรช mแป—i hร ng mแป™t tรชn ngฦฐแปi dรนng" +caseSensitive: "Trฦฐแปng hแปฃp nhแบกy cแบฃm" +withReplies: "Bao gแป“m lฦฐแปฃt trแบฃ lแปi" +connectedTo: "Nhแปฏng tร i khoแบฃn sau ฤ‘รฃ kแบฟt nแป‘i" +notesAndReplies: "Tรบt kรจm trแบฃ lแปi" +withFiles: "Media" +silence: "แบจn" +silenceConfirm: "Bแบกn cรณ chแบฏc muแป‘n แบฉn ngฦฐแปi nร y?" +unsilence: "Bแป แบฉn" +unsilenceConfirm: "Bแบกn cรณ chแบฏc muแป‘n bแป แบฉn ngฦฐแปi nร y?" +popularUsers: "Nhแปฏng ngฦฐแปi nแป•i tiแบฟng" +recentlyUpdatedUsers: "Hoแบกt ฤ‘แป™ng gแบงn ฤ‘รขy" +recentlyRegisteredUsers: "Mแป›i tham gia" +recentlyDiscoveredUsers: "Mแป›i khรกm phรก" +exploreUsersCount: "Cรณ {count} ngฦฐแปi" +exploreFediverse: "Khรกm phรก Fediverse" +popularTags: "Hashtag thรดng dแปฅng" +userList: "Danh sรกch" +about: "Giแป›i thiแป‡u" aboutMisskey: "Vแป Misskey" +administrator: "Quแบฃn trแป‹ viรชn" +token: "Token" +twoStepAuthentication: "Xรกc minh 2 bฦฐแป›c" +moderator: "Kiแปƒm duyแป‡t viรชn" +nUsersMentioned: "Dรนng bแปŸi {n} ngฦฐแปi" +securityKey: "Khรณa bแบฃo mแบญt" +securityKeyName: "Tรชn khoรก" +registerSecurityKey: "ฤฤƒng kรฝ khรณa bแบฃo mแบญt" +lastUsed: "Dรนng lแบงn cuแป‘i" +unregister: "Hแปงy ฤ‘ฤƒng kรฝ" +passwordLessLogin: "ฤฤƒng nhแบญp khรดng mแบญt khแบฉu" +resetPassword: "ฤแบทt lแบกi mแบญt khแบฉu" +newPasswordIs: "Mแบญt khแบฉu mแป›i lร  \"{password}\"" +reduceUiAnimation: "Giแบฃm chuyแปƒn ฤ‘แป™ng UI" +share: "Chia sแบป" +notFound: "Khรดng tรฌm thแบฅy" +notFoundDescription: "Khรดng tรฌm thแบฅy trang nร o tฦฐฦกng แปฉng vแป›i URL nร y." +uploadFolder: "Thฦฐ mแปฅc tแบฃi lรชn mแบทc ฤ‘แป‹nh" +cacheClear: "Xรณa bแป™ nhแป› ฤ‘แป‡m" +markAsReadAllNotifications: "ฤรกnh dแบฅu tแบฅt cแบฃ cรกc thรดng bรกo lร  ฤ‘รฃ ฤ‘แปc" +markAsReadAllUnreadNotes: "ฤรกnh dแบฅu tแบฅt cแบฃ cรกc tรบt lร  ฤ‘รฃ ฤ‘แปc" +markAsReadAllTalkMessages: "ฤรกnh dแบฅu tแบฅt cแบฃ cรกc tin nhแบฏn lร  ฤ‘รฃ ฤ‘แปc" +help: "Trแปฃ giรบp" +inputMessageHere: "Nhแบญp nแป™i dung tin nhแบฏn" +close: "ฤรณng" +group: "Nhรณm" +groups: "Cรกc nhรณm" +createGroup: "Tแบกo nhรณm" +ownedGroups: "Nhรณm tรดi quแบฃn lรฝ" +joinedGroups: "Nhรณm tรดi tham gia" +invites: "Mแปi" +groupName: "Tรชn nhรณm" +members: "Thร nh viรชn" +transfer: "Chuyแปƒn giao" +messagingWithUser: "Nhแบฏn riรชng" +messagingWithGroup: "Chat nhรณm" +title: "Tแปฑa ฤ‘แป" +text: "Nแป™i dung" +enable: "Bแบญt" +next: "Kแบฟ tiแบฟp" +retype: "Nhแบญp lแบกi" +noteOf: "Tรบt cแปงa {user}" +inviteToGroup: "Mแปi vร o nhรณm" +quoteAttached: "Trรญch dแบซn" +quoteQuestion: "Trรญch dแบซn lแบกi?" +noMessagesYet: "Chฦฐa cรณ tin nhแบฏn" +newMessageExists: "Bแบกn cรณ tin nhแบฏn mแป›i" +onlyOneFileCanBeAttached: "Bแบกn chแป‰ cรณ thแปƒ ฤ‘รญnh kรจm mแป™t tแบญp tin" +signinRequired: "Vui lรฒng ฤ‘ฤƒng nhแบญp" +invitations: "Mแปi" +invitationCode: "Mรฃ mแปi" +checking: "ฤang kiแปƒm tra..." +available: "Khแบฃ dแปฅng" +unavailable: "Khรดng khแบฃ dแปฅng" +usernameInvalidFormat: "Bแบกn cรณ thแปƒ dรนng viแบฟt hoa/viแบฟt thฦฐแปng, chแปฏ sแป‘, vร  dแบฅu gแบกch dฦฐแป›i." +tooShort: "Quรก ngแบฏn" +tooLong: "Quรก dร i" +weakPassword: "Mแบญt khแบฉu yแบฟu" +normalPassword: "Mแบญt khแบฉu tแบกm ฤ‘ฦฐแปฃc" +strongPassword: "Mแบญt khแบฉu mแบกnh" +passwordMatched: "Trรนng khแป›p" +passwordNotMatched: "Khรดng trรนng khแป›p" +signinWith: "ฤฤƒng nhแบญp bแบฑng {x}" +signinFailed: "Khรดng thแปƒ ฤ‘ฤƒng nhแบญp. Vui lรฒng kiแปƒm tra tรชn ngฦฐแปi dรนng vร  mแบญt khแบฉu cแปงa bแบกn." +tapSecurityKey: "Nhแบฅn mรฃ bแบฃo mแบญt cแปงa bแบกn" +or: "Hoแบทc" +language: "Ngรดn ngแปฏ" +uiLanguage: "Ngรดn ngแปฏ giao diแป‡n" +groupInvited: "Bแบกn ฤ‘รฃ ฤ‘ฦฐแปฃc mแปi tham gia nhรณm" +aboutX: "Giแป›i thiแป‡u {x}" +useOsNativeEmojis: "Dรนng emoji hแป‡ thแป‘ng" +disableDrawer: "Khรดng dรนng menu thanh bรชn" +youHaveNoGroups: "Khรดng cรณ nhรณm nร o" +joinOrCreateGroup: "Tham gia hoแบทc tแบกo mแป™t nhรณm mแป›i." +noHistory: "Khรดng cรณ dแปฏ liแป‡u" +signinHistory: "Lแป‹ch sแปญ ฤ‘ฤƒng nhแบญp" +disableAnimatedMfm: "Tแบฏt MFM vแป›i chuyแปƒn ฤ‘แป™ng" +doing: "ฤang xแปญ lรฝ..." +category: "Phรขn loแบกi" +tags: "Thแบป" +docSource: "Nguแป“n tร i liแป‡u" +createAccount: "Tแบกo tร i khoแบฃn" +existingAccount: "Tร i khoแบฃn hiแป‡n cรณ" +regenerate: "Tแบกo lแบกi" +fontSize: "Cแปก chแปฏ" +noFollowRequests: "Bแบกn khรดng cรณ yรชu cแบงu theo dรตi nร o" +openImageInNewTab: "MแปŸ แบฃnh trong tab mแป›i" +dashboard: "Trang chรญnh" +local: "Mรกy chแปง nร y" +remote: "Mรกy chแปง khรกc" +total: "Tแป•ng cแป™ng" +weekOverWeekChanges: "Thay ฤ‘แป•i tuแบงn rแป“i" +dayOverDayChanges: "Thay ฤ‘แป•i hรดm qua" +appearance: "Giao diแป‡n" +clientSettings: "Cร i ฤ‘แบทt Client" +accountSettings: "Cร i ฤ‘แบทt tร i khoแบฃn" +promotion: "Quแบฃng cรกo" +promote: "Quแบฃng cรกo" +numberOfDays: "Sแป‘ ngร y" +hideThisNote: "แบจn tรบt nร y" +showFeaturedNotesInTimeline: "Hiแป‡n tรบt nแป•i bแบญt trong bแบฃng tin" +objectStorage: "ฤแป‘i tฦฐแปฃng lฦฐu trแปฏ" +useObjectStorage: "Dรนng ฤ‘แป‘i tฦฐแปฃng lฦฐu trแปฏ" +objectStorageBaseUrl: "Base URL" +objectStorageBaseUrlDesc: "URL ฤ‘ฦฐแปฃc sแปญ dแปฅng lร m tham khแบฃo. Chแป‰ ฤ‘แป‹nh URL cแปงa CDN hoแบทc Proxy cแปงa bแบกn nแบฟu bแบกn ฤ‘ang sแปญ dแปฅng. Vแป›i S3 dรนng 'https://.s3.amazonaws.com', cรฒn GCS hoแบทc dแป‹ch vแปฅ tฦฐฦกng tแปฑ dรนng 'https://storage.googleapis.com/', etc." +objectStorageBucket: "Bucket" +objectStorageBucketDesc: "Nhแบญp tรชn bucket dรนng แปŸ nhร  cung cแบฅp cแปงa bแบกn." +objectStoragePrefix: "Tiแปn tแป‘" +objectStoragePrefixDesc: "Cรกc tแบญp tin sแบฝ ฤ‘ฦฐแปฃc lฦฐu trแปฏ trong cรกc thฦฐ mแปฅc cรณ tiแปn tแป‘ nร y." +objectStorageEndpoint: "ฤแบงu cuแป‘i" +objectStorageEndpointDesc: "ฤแปƒ trแป‘ng nแบฟu bแบกn ฤ‘ang dรนng AWS S3, nแบฟu khรดng thรฌ chแป‰ ฤ‘แป‹nh ฤ‘แบงu cuแป‘i lร  '' hoแบทc ':', tรนy thuแป™c vร o nhร  cung cแบฅp dแป‹ch vแปฅ." +objectStorageRegion: "Khu vแปฑc" +objectStorageRegionDesc: "Nhแบญp mแป™t khu vแปฑc cแปฅ thแปƒ nhฦฐ 'xx-east-1'. Nแบฟu nhร  cung cแบฅp dแป‹ch vแปฅ cแปงa bแบกn khรดng phรขn biแป‡t giแปฏa cรกc khu vแปฑc, hรฃy ฤ‘แปƒ trแป‘ng hoแบทc nhแบญp 'us-east-1'." +objectStorageUseSSL: "Dรนng SSL" +objectStorageUseSSLDesc: "Tแบฏt nแบฟu bแบกn khรดng dรนng HTTPS ฤ‘แปƒ kแบฟt nแป‘i API" +objectStorageUseProxy: "Kแบฟt nแป‘i thรดng qua Proxy" +objectStorageUseProxyDesc: "Tแบฏt nแบฟu bแบกn khรดng dรนng Proxy ฤ‘แปƒ kแบฟt nแป‘i API" +objectStorageSetPublicRead: "ฤแบทt \"public-read\" khi tแบฃi lรชn" +serverLogs: "Nhแบญt kรฝ mรกy chแปง" +deleteAll: "Xรณa tแบฅt cแบฃ" +showFixedPostForm: "Hiแป‡n khung soแบกn tรบt แปŸ phรญa trรชn bแบฃng tin" +newNoteRecived: "ฤรฃ nhแบญn tรบt mแป›i" +sounds: "ร‚m thanh" +listen: "Nghe" +none: "Khรดng" +showInPage: "Hiแป‡n trong trang" +popout: "Pop-out" +volume: "ร‚m lฦฐแปฃng" +masterVolume: "ร‚m thanh chung" +details: "Chi tiแบฟt" +chooseEmoji: "Chแปn emoji" +unableToProcess: "Khรดng thแปƒ hoร n tแบฅt hร nh ฤ‘แป™ng" +recentUsed: "Sแปญ dแปฅng gแบงn ฤ‘รขy" +install: "Cร i ฤ‘แบทt" +uninstall: "Gแปก bแป" +installedApps: "แปจng dแปฅng ฤ‘รฃ cร i ฤ‘แบทt" +nothing: "Khรดng cรณ gรฌ แปŸ ฤ‘รขy" +installedDate: "Cho phรฉp vร o" +lastUsedDate: "Dรนng gแบงn nhแบฅt" +state: "Trแบกng thรกi" +sort: "Sแบฏp xแบฟp" +ascendingOrder: "Tฤƒng dแบงn" +descendingOrder: "Giแบฃm dแบงn" +scratchpad: "Scratchpad" +scratchpadDescription: "Scratchpad cung cแบฅp mรดi trฦฐแปng cho cรกc thแปญ nghiแป‡m AiScript. Bแบกn cรณ thแปƒ viแบฟt, thแปฑc thi vร  kiแปƒm tra kแบฟt quแบฃ tฦฐฦกng tรกc vแป›i Misskey trong ฤ‘รณ." +output: "Nguแป“n ra" +script: "Kแป‹ch bแบฃn" +disablePagesScript: "Tแบฏt AiScript trรชn Trang" +updateRemoteUser: "Cแบญp nhแบญt thรดng tin ngฦฐแปi dรนng แปŸ mรกy chแปง khรกc" +deleteAllFiles: "Xรณa toร n bแป™ tแบญp tin" +deleteAllFilesConfirm: "Bแบกn cรณ chแบฏc xรณa toร n bแป™ tแบญp tin?" +removeAllFollowing: "Ngฦฐng theo dรตi tแบฅt cแบฃ mแปi ngฦฐแปi" +removeAllFollowingDescription: "Thแปฑc hiแป‡n ฤ‘iแปu nร y sแบฝ ngฦฐng theo dรตi tแบฅt cแบฃ cรกc tร i khoแบฃn khแปi {host}. Chแป‰ thแปฑc hiแป‡n ฤ‘iแปu nร y nแบฟu mรกy chแปง khรดng cรฒn tแป“n tแบกi." +userSuspended: "Ngฦฐแปi nร y ฤ‘รฃ bแป‹ vรด hiแป‡u hรณa." +userSilenced: "Ngฦฐแปi nร y ฤ‘รฃ bแป‹ แบฉn" +yourAccountSuspendedTitle: "Tร i khoแบฃn bแป‹ vรด hiแป‡u hรณa" +yourAccountSuspendedDescription: "Tร i khoแบฃn nร y ฤ‘รฃ bแป‹ vรด hiแป‡u hรณa do vi phแบกm quy tแบฏc mรกy chแปง hoแบทc ฤ‘iแปu tฦฐฦกng tแปฑ. Liรชn hแป‡ vแป›i quแบฃn trแป‹ viรชn nแบฟu bแบกn muแป‘n biแบฟt lรฝ do chi tiแบฟt hฦกn. Vui lรฒng khรดng tแบกo tร i khoแบฃn mแป›i." +menu: "Menu" +divider: "Phรขn chia" +addItem: "Thรชm mแปฅc" +relays: "Chuyแปƒn tiแบฟp" +addRelay: "Thรชm chuyแปƒn tiแบฟp" +inboxUrl: "URL Hแป™p thฦฐ ฤ‘แบฟn" +addedRelays: "ฤรฃ thรชm cรกc chuyแปƒn tiแบฟp" +serviceworkerInfo: "Phแบฃi ฤ‘ฦฐแปฃc bแบญt cho thรดng bรกo ฤ‘แบฉy." +deletedNote: "Tรบt ฤ‘รฃ bแป‹ xรณa" +invisibleNote: "Tรบt รขฬ‰n" +enableInfiniteScroll: "Tแปฑ ฤ‘แป™ng tแบฃi tรบt mแป›i" +visibility: "Hiแปƒn thแป‹" +poll: "Bรฌnh chแปn" +useCw: "แบจn nแป™i dung" +enablePlayer: "MแปŸ trรฌnh phรกt video" +disablePlayer: "ฤรณng trรฌnh phรกt video" +expandTweet: "MแปŸ rแป™ng tweet" +themeEditor: "Cรดng cแปฅ thiแบฟt kแบฟ theme" +description: "Mรด tแบฃ" +describeFile: "Thรชm mรด tแบฃ" +enterFileDescription: "Nhแบญp mรด tแบฃ" +author: "Tรกc giแบฃ" +leaveConfirm: "Cรณ nhแปฏng thay ฤ‘แป•i chฦฐa ฤ‘ฦฐแปฃc lฦฐu. Bแบกn cรณ muแป‘n bแป chรบng khรดng?" +manage: "Quแบฃn lรฝ" +plugins: "Plugin" +deck: "Deck" +undeck: "Bแป Deck" +useBlurEffectForModal: "Sแปญ dแปฅng hiแป‡u แปฉng mแป cho cรกc hแป™p thoแบกi" +useFullReactionPicker: "Dรนng bแป™ chแปn biแปƒu cแบฃm cแปก lแป›n" +width: "Chiแปu rแป™ng" +height: "Chiแปu cao" +large: "Lแป›n" +medium: "Vแปซa" +small: "Nhแป" +generateAccessToken: "Tแบกo mรฃ truy cแบญp" +permission: "Cho phรฉp " +enableAll: "Bแบญt toร n bแป™" +disableAll: "Tแบฏt toร n bแป™" +tokenRequested: "Cแบฅp quyแปn truy cแบญp vร o tร i khoแบฃn" +pluginTokenRequestedDescription: "Plugin nร y sแบฝ cรณ thแปƒ sแปญ dแปฅng cรกc quyแปn ฤ‘ฦฐแปฃc ฤ‘แบทt แปŸ ฤ‘รขy." +notificationType: "Loแบกi thรดng bรกo" +edit: "Sแปญa" +useStarForReactionFallback: "Dรนng โ˜… nแบฟu emoji biแปƒu cแบฃm khรดng cรณ" +emailServer: "Email mรกy chแปง" +enableEmail: "Bแบญt phรขn phแป‘i email" +emailConfigInfo: "ฤฦฐแปฃc dรนng ฤ‘แปƒ xรกc minh email cแปงa bแบกn lรบc ฤ‘ฤƒng kรฝ hoแบทc nแบฟu bแบกn quรชn mแบญt khแบฉu cแปงa mรฌnh" +email: "Email" +emailAddress: "ฤแป‹a chแป‰ email" +smtpConfig: "Cแบฅu hรฌnh mรกy chแปง SMTP" +smtpHost: "Host" +smtpPort: "Cแป•ng" smtpUser: "Tรชn ngฦฐแปi dรนng" smtpPass: "Mแบญt khแบฉu" +emptyToDisableSmtpAuth: "ฤแปƒ trแป‘ng tรชn ngฦฐแปi dรนng vร  mแบญt khแบฉu ฤ‘แปƒ tแบฏt xรกc thแปฑc SMTP" +smtpSecure: "Dรนng SSL/TLS ngแบงm ฤ‘แป‹nh cho cรกc kแบฟt nแป‘i SMTP" +smtpSecureInfo: "Tแบฏt cรกi nร y nแบฟu dรนng STARTTLS" +testEmail: "Kiแปƒm tra vแบญn chuyแปƒn email" +wordMute: "แบจn chแปฏ" +regexpError: "Lแป—i biแปƒu thแปฉc" +regexpErrorDescription: "Xแบฃy ra lแป—i biแปƒu thแปฉc แปŸ dรฒng {line} cแปงa {tab} chแปฏ แบฉn:" +instanceMute: "Nhแปฏng mรกy chแปง แบฉn" +userSaysSomething: "{name} nรณi gรฌ ฤ‘รณ" +makeActive: "Kรญch hoแบกt" +display: "Hiแปƒn thแป‹" +copy: "Sao chรฉp" +metrics: "Sแป‘ liแป‡u" +overview: "Tแป•ng quan" +logs: "Nhแบญt kรฝ" +delayed: "ฤแป™ trแป…" +database: "Cฦก sแปŸ dแปฏ liแป‡u" +channel: "Kรชnh" +create: "Tแบกo" +notificationSetting: "Cร i ฤ‘แบทt thรดng bรกo" +notificationSettingDesc: "Chแปn loแบกi thรดng bรกo bแบกn muแป‘n hiแปƒn thแป‹." +useGlobalSetting: "Dรนng thiแบฟt lแบญp chung" +useGlobalSettingDesc: "Nแบฟu ฤ‘ฦฐแปฃc bแบญt, cร i ฤ‘แบทt thรดng bรกo cแปงa bแบกn sแบฝ ฤ‘ฦฐแปฃc รกp dแปฅng. Nแบฟu bแป‹ tแบฏt, cรณ thแปƒ thแปฑc hiแป‡n cรกc thiแบฟt lแบญp riรชng lแบป." +other: "Khรกc" +regenerateLoginToken: "Tแบกo lแบกi mรฃ ฤ‘ฤƒng nhแบญp" +regenerateLoginTokenDescription: "Tแบกo lแบกi mรฃ nแป™i bแป™ cรณ thแปƒ dรนng ฤ‘แปƒ ฤ‘ฤƒng nhแบญp. Thรดng thฦฐแปng hร nh ฤ‘แป™ng nร y lร  khรดng cแบงn thiแบฟt. Nแบฟu ฤ‘ฦฐแปฃc tแบกo lแบกi, tแบฅt cแบฃ cรกc thiแบฟt bแป‹ sแบฝ bแป‹ ฤ‘ฤƒng xuแบฅt." +setMultipleBySeparatingWithSpace: "Tรกch nhiแปu mแปฅc nhแบญp bแบฑng dแบฅu cรกch." +fileIdOrUrl: "ID tแบญp tin hoแบทc URL" +behavior: "Thao tรกc" +sample: "Vรญ dแปฅ" +abuseReports: "Lฦฐแปฃt bรกo cรกo" +reportAbuse: "Bรกo cรกo" reportAbuseOf: "Bรกo cรกo {name}" +fillAbuseReportDescription: "Vui lรฒng ฤ‘iแปn thรดng tin chi tiแบฟt vแป bรกo cรกo nร y. Nแบฟu ฤ‘รณ lร  vแป mแป™t tรบt cแปฅ thแปƒ, hรฃy kรจm theo URL cแปงa tรบt." +abuseReported: "Bรกo cรกo ฤ‘รฃ ฤ‘ฦฐแปฃc gแปญi. Cแบฃm ฦกn bแบกn nhiแปu." +reporter: "Ngฦฐแปi bรกo cรกo" +reporteeOrigin: "Bแป‹ bรกo cรกo" +reporterOrigin: "Mรกy chแปง ngฦฐแปi bรกo cรกo" +forwardReport: "Chuyแปƒn tiแบฟp bรกo cรกo cho mรกy chแปง tแปซ xa" +forwardReportIsAnonymous: "Thay vรฌ tร i khoแบฃn cแปงa bแบกn, mแป™t tร i khoแบฃn hแป‡ thแป‘ng แบฉn danh sแบฝ ฤ‘ฦฐแปฃc hiแปƒn thแป‹ dฦฐแป›i dแบกng ngฦฐแปi bรกo cรกo แปŸ mรกy chแปง tแปซ xa." +send: "Gแปญi" +abuseMarkAsResolved: "ฤรกnh dแบฅu ฤ‘รฃ xแปญ lรฝ" +openInNewTab: "MแปŸ trong tab mแป›i" +openInSideView: "MแปŸ trong thanh bรชn" +defaultNavigationBehaviour: "Thao tรกc ฤ‘iแปu hฦฐแป›ng mแบทc ฤ‘แป‹nh" +editTheseSettingsMayBreakAccount: "Viแป‡c chแป‰nh sแปญa cรกc cร i ฤ‘แบทt nร y cรณ thแปƒ lร m hแปng tร i khoแบฃn cแปงa bแบกn." +instanceTicker: "Thรดng tin mรกy chแปง cแปงa tรบt" +waitingFor: "ฤang ฤ‘แปฃi {x}" +random: "Ngแบซu nhiรชn" +system: "Hแป‡ thแป‘ng" +switchUi: "Chuyแปƒn ฤ‘แป•i giao diแป‡n ngฦฐแปi dรนng" +desktop: "Desktop" +clip: "Ghim" +createNew: "Tแบกo mแป›i" +optional: "Khรดng bแบฏt buแป™c" +createNewClip: "Tแบกo mแป™t ghim mแป›i" +public: "Cรดng khai" +i18nInfo: "Misskey ฤ‘ang ฤ‘ฦฐแปฃc cรกc tรฌnh nguyแป‡n viรชn dแป‹ch sang nhiแปu thแปฉ tiแบฟng khรกc nhau. Bแบกn cรณ thแปƒ hแป— trแปฃ tแบกi {link}." +manageAccessTokens: "Tแบกo mรฃ truy cแบญp" +accountInfo: "Thรดng tin tร i khoแบฃn" +notesCount: "Sแป‘ lฦฐแปฃng tรบt" +repliesCount: "Sแป‘ lฦฐแปฃt trแบฃ lแปi ฤ‘รฃ gแปญi" +renotesCount: "Sแป‘ lฦฐแปฃt ฤ‘ฤƒng lแบกi ฤ‘รฃ gแปญi" +repliedCount: "Sแป‘ lฦฐแปฃt trแบฃ lแปi ฤ‘รฃ nhแบญn" renotedCount: "Lฦฐแปฃt chia sแบป" +followingCount: "Sแป‘ lฦฐแปฃng ngฦฐแปi tรดi theo dรตi" +followersCount: "Sแป‘ lฦฐแปฃng ngฦฐแปi theo dรตi tรดi" +sentReactionsCount: "Sแป‘ lฦฐแปฃng biแปƒu cแบฃm ฤ‘รฃ gแปญi" +receivedReactionsCount: "Sแป‘ lฦฐแปฃng biแปƒu cแบฃm ฤ‘รฃ nhแบญn" +pollVotesCount: "Sแป‘ lฦฐแปฃng bรฌnh chแปn ฤ‘รฃ gแปญi" +pollVotedCount: "Sแป‘ lฦฐแปฃng bรฌnh chแปn ฤ‘รฃ nhแบญn" +yes: "ฤแป“ng รฝ" +no: "Tแปซ chแป‘i" +driveFilesCount: "Sแป‘ tแบญp tin trong แป” ฤ‘ฤฉa" +driveUsage: "Dung lฦฐแปฃng แป• ฤ‘ฤฉa" +noCrawle: "Tแปซ chแป‘i lแบญp chแป‰ mแปฅc" +noCrawleDescription: "Khรดng cho cรดng cแปฅ tรฌm kiแบฟm lแบญp chแป‰ mแปฅc trang hแป“ sฦก, tรบt, Trang, etc." +lockedAccountInfo: "Ghi chรบ cแปงa bแบกn sแบฝ hiแปƒn thแป‹ vแป›i bแบฅt kแปณ ai, trแปซ khi bแบกn ฤ‘แบทt chแบฟ ฤ‘แป™ hiแปƒn thแป‹ tรบt cแปงa mรฌnh thร nh \"Chแป‰ ngฦฐแปi theo dรตi\"." +alwaysMarkSensitive: "Luรดn ฤ‘รกnh dแบฅu NSFW" +loadRawImages: "Tแบฃi แบฃnh gแป‘c thay vรฌ แบฃnh thu nhแป" +disableShowingAnimatedImages: "Khรดng phรกt แบฃnh ฤ‘แป™ng" +verificationEmailSent: "Mแป™t email xรกc minh ฤ‘รฃ ฤ‘ฦฐแปฃc gแปญi. Vui lรฒng nhแบฅn vร o liรชn kแบฟt ฤ‘รญnh kรจm ฤ‘แปƒ hoร n tแบฅt xรกc minh." +notSet: "Chฦฐa ฤ‘แบทt" +emailVerified: "Email ฤ‘รฃ ฤ‘ฦฐแปฃc xรกc minh" +noteFavoritesCount: "Sแป‘ lฦฐแปฃng tรบt yรชu thรญch" +pageLikesCount: "Sแป‘ lฦฐแปฃng trang ฤ‘รฃ thรญch" +pageLikedCount: "Sแป‘ lฦฐแปฃng thรญch trang ฤ‘รฃ nhแบญn" +contact: "Liรชn hแป‡" +useSystemFont: "Dรนng phรดng chแปฏ mแบทc ฤ‘แป‹nh cแปงa hแป‡ thแป‘ng" +clips: "Ghim" +experimentalFeatures: "Tรญnh nฤƒng thแปญ nghiแป‡m" +developer: "Nhร  phรกt triแปƒn" +makeExplorable: "Khรดng hiแป‡n tรดi trong \"Khรกm phรก\"" +makeExplorableDescription: "Nแบฟu bแบกn tแบฏt, tร i khoแบฃn cแปงa bแบกn sแบฝ khรดng hiแป‡n trong mแปฅc \"Khรกm phรก\"." +showGapBetweenNotesInTimeline: "Hiแป‡n dแบฃi phรขn cรกch giแปฏa cรกc tรบt trรชn bแบฃng tin" +duplicate: "Tแบกo bแบฃn sao" +left: "Bรชn traฬi" +center: "Giแปฏa" +wide: "Rแป™ng" +narrow: "Thu hแบนp" +reloadToApplySetting: "Cร i ฤ‘แบทt nร y sแบฝ chแป‰ รกp dแปฅng sau khi tแบฃi lแบกi trang. Tแบฃi lแบกi ngay bรขy giแป?" +needReloadToApply: "Cแบงn tแบฃi lแบกi ฤ‘แปƒ ฤ‘iแปu nร y ฤ‘ฦฐแปฃc รกp dแปฅng." +showTitlebar: "Hiแป‡n thanh tแปฑa ฤ‘แป" +clearCache: "Xรณa bแป™ nhแป› ฤ‘แป‡m" +onlineUsersCount: "{n} ngฦฐแปi ฤ‘ang online" +nUsers: "{n} Ngฦฐแปi" +nNotes: "{n} Tรบt" +sendErrorReports: "Bรกo lแป—i" +sendErrorReportsDescription: "Khi ฤ‘ฦฐแปฃc bแบญt, thรดng tin chi tiแบฟt vแป lแป—i sแบฝ ฤ‘ฦฐแปฃc chia sแบป vแป›i Misskey khi xแบฃy ra sแปฑ cแป‘, giรบp nรขng cao chแบฅt lฦฐแปฃng cแปงa Misskey.\nBao gแป“m thรดng tin nhฦฐ phiรชn bแบฃn hแป‡ ฤ‘iแปu hร nh cแปงa bแบกn, trรฌnh duyแป‡t bแบกn ฤ‘ang sแปญ dแปฅng, hoแบกt ฤ‘แป™ng cแปงa bแบกn trong Misskey, v.v." +myTheme: "Theme cแปงa tรดi" +backgroundColor: "Mร u nแปn" +accentColor: "Mร u phแปฅ" +textColor: "Mร u chแปฏ" +saveAs: "Lฦฐu thร nh" +advanced: "Nรขng cao" +value: "Giรก trแป‹" +createdAt: "Ngร y tแบกo" +updatedAt: "Cแบญp nhแบญt lรบc" +saveConfirm: "Lฦฐu thay ฤ‘แป•i?" +deleteConfirm: "Bแบกn cรณ muแป‘n xรณa khรดng?" +invalidValue: "Giรก trแป‹ khรดng hแปฃp lแป‡." +registry: "Registry" +closeAccount: "ฤรณng tร i khoแบฃn" +currentVersion: "Phiรชn bแบฃn hiแป‡n tแบกi" +latestVersion: "Phiรชn bแบฃn mแป›i nhแบฅt" +youAreRunningUpToDateClient: "Bแบกn ฤ‘ang sแปญ dแปฅng phiรชn bแบฃn mแป›i nhแบฅt." +newVersionOfClientAvailable: "Cรณ phiรชn bแบฃn mแป›i cho bแบกn cแบญp nhแบญt." +usageAmount: "Sแปญ dแปฅng" +capacity: "Sแปฉc chแปฉa" +inUse: "ฤรฃ dรนng" +editCode: "Chแป‰nh sแปญa mรฃ" +apply: "รp dแปฅng" +receiveAnnouncementFromInstance: "Nhแบญn thรดng bรกo tแปซ mรกy chแปง nร y" +emailNotification: "Thรดng bรกo email" +publish: "ฤฤƒng" +inChannelSearch: "Tรฌm trong kรชnh" +useReactionPickerForContextMenu: "Nhแบฅn chuแป™t phแบฃi ฤ‘แปƒ mแปŸ bแป™ chแปn biแปƒu cแบฃm" +typingUsers: "{users} ฤ‘ang nhแบญpโ€ฆ" +jumpToSpecifiedDate: "ฤแบฟn mแป™t ngร y cแปฅ thแปƒ" +showingPastTimeline: "Hiแป‡n ฤ‘ang hiแปƒn thแป‹ dรฒng thแปi gian cลฉ" +clear: "Hoร n lแบกi" +markAllAsRead: "ฤรกnh dแบฅu tแบฅt cแบฃ ฤ‘รฃ ฤ‘แปc" +goBack: "Quay lแบกi" +unlikeConfirm: "Bแบกn cรณ chแบฏc muแป‘n bแป thรญch ?" +fullView: "Kรญch thฦฐแป›c ฤ‘แบงy ฤ‘แปง" +quitFullView: "Thoรกt toร n mร n hรฌnh" +addDescription: "Thรชm mรด tแบฃ" +userPagePinTip: "Bแบกn cรณ thแปƒ hiแปƒn thแป‹ cรกc tรบt แปŸ ฤ‘รขy bแบฑng cรกch chแปn \"Ghim vร o hแป“ sฦก\" tแปซ menu cแปงa mแป—i tรบt." +notSpecifiedMentionWarning: "Tรบt nร y cรณ ฤ‘แป cแบญp ฤ‘แบฟn nhแปฏng ngฦฐแปi khรดng mong muแป‘n" +info: "Giแป›i thiแป‡u" +userInfo: "Thรดng tin ngฦฐแปi dรนng" +unknown: "Chฦฐa biแบฟt" +onlineStatus: "Trแบกng thรกi" +hideOnlineStatus: "แบจn trแบกng thรกi online" +hideOnlineStatusDescription: "แบจn trแบกng thรกi online cแปงa bแบกn lร m giแบฃm sแปฑ tiแป‡n lแปฃi cแปงa mแป™t sแป‘ tรญnh nฤƒng nhฦฐ tรฌm kiแบฟm." +online: "Online" +active: "Hoแบกt ฤ‘แป™ng" +offline: "Offline" +notRecommended: "Khรดng ฤ‘แป xuแบฅt" +botProtection: "Bแบฃo vแป‡ Bot" +instanceBlocking: "Mรกy chแปง ฤ‘รฃ chแบทn" +selectAccount: "Chแปn mแป™t tร i khoแบฃn" +switchAccount: "Chuyแปƒn tร i khoแบฃn" +enabled: "ฤรฃ bแบญt" +disabled: "ฤรฃ tแบฏt" +quickAction: "Thao tรกc nhanh" +user: "Ngฦฐแปi dรนng" +administration: "Quแบฃn lรฝ" +accounts: "Tร i khoแบฃn cแปงa bแบกn" +switch: "Chuyแปƒn ฤ‘แป•i" +noMaintainerInformationWarning: "Chฦฐa thiแบฟt lแบญp thรดng tin vแบญn hร nh." +noBotProtectionWarning: "Bแบฃo vแป‡ Bot chฦฐa thiแบฟt lแบญp." +configure: "Thiแบฟt lแบญp" +postToGallery: "Tแบกo tรบt cรณ แบฃnh" +gallery: "Thฦฐ viแป‡n แบฃnh" +recentPosts: "Tรบt gแบงn ฤ‘รขy" +popularPosts: "Tรบt ฤ‘ฦฐแปฃc xem nhiแปu nhแบฅt" +shareWithNote: "Chia sแบป kรจm vแป›i tรบt" +ads: "Quแบฃng cรกo" +expiration: "Thแปi hแบกn" +memo: "Lฦฐu รฝ" +priority: "ฦฏu tiรชn" +high: "Cao" +middle: "Vแปซa" +low: "Thแบฅp" +emailNotConfiguredWarning: "Chฦฐa ฤ‘แบทt ฤ‘แป‹a chแป‰ email." +ratio: "Tแปท lแป‡" +previewNoteText: "Hiแป‡n xem trฦฐแป›c" +customCss: "Tรนy chแป‰nh CSS" +customCssWarn: "Chแป‰ sแปญ dแปฅng nhแปฏng cร i ฤ‘แบทt nร y nแบฟu bแบกn biแบฟt rรต vแป nรณ. Viแป‡c nhแบญp cรกc giรก trแป‹ khรดng ฤ‘รบng cรณ thแปƒ khiแบฟn mรกy chแปง hoแบกt ฤ‘แป™ng khรดng bรฌnh thฦฐแปng." +global: "Toร n cแบงu" +squareAvatars: "แบขnh ฤ‘แบกi diแป‡n vuรดng" +sent: "Gแปญi" +received: "ฤรฃ nhแบญn" +searchResult: "Kแบฟt quแบฃ tรฌm kiแบฟm" +hashtags: "Hashtag" +troubleshooting: "Khแบฏc phแปฅc sแปฑ cแป‘" +useBlurEffect: "Dรนng hiแป‡u แปฉng lร m mแป trong giao diแป‡n" +learnMore: "Tรฌm hiแปƒu thรชm" +misskeyUpdated: "Misskey vแปซa ฤ‘ฦฐแปฃc cแบญp nhแบญt!" +whatIsNew: "Hiแป‡n nhแปฏng thay ฤ‘แป•i" +translate: "Dแป‹ch" translatedFrom: "Dแป‹ch tแปซ {x}" +accountDeletionInProgress: "ฤang xแปญ lรฝ viแป‡c xรณa tร i khoแบฃn" +usernameInfo: "Bแบกn cรณ thแปƒ sแปญ dแปฅng chแปฏ cรกi (a ~ z, A ~ Z), chแปฏ sแป‘ (0 ~ 9) hoแบทc dแบฅu gแบกch dฦฐแป›i (_). Tรชn ngฦฐแปi dรนng khรดng thแปƒ thay ฤ‘แป•i sau nร y." +aiChanMode: "Chแบฟ ฤ‘แป™ Ai" +keepCw: "Giแปฏ cแบฃnh bรกo nแป™i dung" +pubSub: "Tร i khoแบฃn Chรญnh/Phแปฅ" +lastCommunication: "Lแบงn giao tiแบฟp cuแป‘i" +resolved: "ฤรฃ xแปญ lรฝ" +unresolved: "Chแป xแปญ lรฝ" +breakFollow: "Xรณa ngฦฐแปi theo dรตi" +itsOn: "ฤรฃ bแบญt" +itsOff: "ฤรฃ tแบฏt" +emailRequiredForSignup: "Yรชu cแบงu ฤ‘แป‹a chแป‰ email khi ฤ‘ฤƒng kรฝ" +unread: "Chฦฐa ฤ‘แปc" +filter: "Bแป™ lแปc" +controlPanel: "Bแบฃng ฤ‘iแปu khiแปƒn" +manageAccounts: "Quแบฃn lรฝ tร i khoแบฃn" +makeReactionsPublic: "ฤแบทt lแป‹ch sแปญ biแปƒu cแบฃm cรดng khai" +makeReactionsPublicDescription: "ฤiแปu nร y sแบฝ hiแปƒn thแป‹ cรดng khai danh sรกch tแบฅt cแบฃ cรกc biแปƒu cแบฃm trฦฐแป›c ฤ‘รขy cแปงa bแบกn." +classic: "Cแป• ฤ‘iแปƒn" +muteThread: "Khรดng quan tรขm nแปฏa" +unmuteThread: "Quan tรขm tรบt nร y" +ffVisibility: "Hiแปƒn thแป‹ Theo dรตi/Ngฦฐแปi theo dรตi" +ffVisibilityDescription: "Quyแบฟt ฤ‘แป‹nh ai cรณ thแปƒ xem nhแปฏng ngฦฐแปi bแบกn theo dรตi vร  nhแปฏng ngฦฐแปi theo dรตi bแบกn." +continueThread: "Tiแบฟp tแปฅc xem chuแป—i tรบt" +deleteAccountConfirm: "ฤiแปu nร y sแบฝ khiแบฟn tร i khoแบฃn bแป‹ xรณa vฤฉnh viแป…n. Vแบซn tiแบฟp tแปฅc?" +incorrectPassword: "Sai mแบญt khแบฉu." +voteConfirm: "Xรกc nhแบญn bรฌnh chแปn \"{choice}\"?" +hide: "แบจn" +leaveGroup: "Rแปi khแปi nhรณm" +leaveGroupConfirm: "Bแบกn cรณ chแบฏc muแป‘n rแปi khแปi nhรณm \"{name}\"?" +useDrawerReactionPickerForMobile: "Hiแป‡n bแป™ chแปn biแปƒu cแบฃm dแบกng xแป• ra trรชn ฤ‘iแป‡n thoแบกi" +welcomeBackWithName: "Chร o mแปซng trแปŸ lแบกi, {name}" +clickToFinishEmailVerification: "Vui lรฒng nhแบฅn [{ok}] ฤ‘แปƒ hoร n tแบฅt viแป‡c ฤ‘ฤƒng kรฝ." +overridedDeviceKind: "Loแบกi thiแบฟt bแป‹" +smartphone: "ฤiแป‡n thoแบกi" +tablet: "Mรกy tรญnh bแบฃng" +auto: "Tแปฑ ฤ‘แป™ng" +themeColor: "Mร u theme" +size: "Kรญch thฦฐแป›c" +numberOfColumn: "Sแป‘ lฦฐแปฃng cแป™t" searchByGoogle: "Google" +instanceDefaultLightTheme: "Theme mรกy chแปง Sรกng-Rแป™ng" +instanceDefaultDarkTheme: "Theme mรกy chแปง Tแป‘i-Rแป™ng" +instanceDefaultThemeDescription: "Nhแบญp mรฃ theme trong ฤ‘แป‹nh dแบกng ฤ‘แป‘i tฦฐแปฃng." +mutePeriod: "Thแปi hแบกn แบฉn" +indefinitely: "Vฤฉnh viแป…n" +tenMinutes: "10 phรบt" +oneHour: "1 giแป" +oneDay: "1 ngร y" +oneWeek: "1 tuแบงn" +reflectMayTakeTime: "Cรณ thแปƒ mแบฅt mแป™t thแปi gian ฤ‘แปƒ ฤ‘iแปu nร y ฤ‘ฦฐแปฃc รกp dแปฅng." +failedToFetchAccountInformation: "Khรดng thแปƒ lแบฅy thรดng tin tร i khoแบฃn" +rateLimitExceeded: "Giแป›i hแบกn quรก mแปฉc" +_emailUnavailable: + used: "ฤแป‹a chแป‰ email ฤ‘รฃ ฤ‘ฦฐแปฃc sแปญ dแปฅng" + format: "ฤแป‹a chแป‰ email khรดng hแปฃp lแป‡" + disposable: "Cแบฅm sแปญ dแปฅng ฤ‘แป‹a chแป‰ email dรนng mแป™t lแบงn" + mx: "Mรกy chแปง email khรดng hแปฃp lแป‡" + smtp: "Mรกy chแปง email khรดng phแบฃn hแป“i" +_ffVisibility: + public: "ฤฤƒng" + followers: "Chแป‰ ngฦฐแปi theo dรตi mแป›i xem ฤ‘ฦฐแปฃc" + private: "Riรชng tฦฐ" +_signup: + almostThere: "Gแบงn xong rแป“i" + emailAddressInfo: "Hรฃy ฤ‘iแปn ฤ‘แป‹a chแป‰ email cแปงa bแบกn. Nรณ sแบฝ khรดng ฤ‘ฦฐแปฃc cรดng khai." + emailSent: "Mแป™t email xรกc minh ฤ‘รฃ ฤ‘ฦฐแปฃc gแปญi ฤ‘แบฟn ฤ‘แป‹a chแป‰ email ({email}) cแปงa bแบกn. Vui lรฒng nhแบฅn vร o liรชn kแบฟt trong ฤ‘รณ ฤ‘แปƒ hoร n tแบฅt viแป‡c tแบกo tร i khoแบฃn." +_accountDelete: + accountDelete: "Xรณa tร i khoแบฃn" + mayTakeTime: "Vรฌ xรณa tร i khoแบฃn lร  mแป™t quรก trรฌnh tแป‘n nhiแปu tร i nguyรชn nรชn cรณ thแปƒ mแบฅt mแป™t khoแบฃng thแปi gian ฤ‘แปƒ hoร n thร nh, tรนy thuแป™c vร o lฦฐแปฃng nแป™i dung bแบกn ฤ‘รฃ tแบกo vร  sแป‘ lฦฐแปฃng tแบญp tin bแบกn ฤ‘รฃ tแบฃi lรชn." + sendEmail: "Sau khi hoร n tแบฅt viแป‡c xรณa tร i khoแบฃn, mแป™t email sแบฝ ฤ‘ฦฐแปฃc gแปญi ฤ‘แบฟn ฤ‘แป‹a chแป‰ email ฤ‘รฃ ฤ‘ฤƒng kรฝ tร i khoแบฃn nร y." + requestAccountDelete: "Yรชu cแบงu xรณa tร i khoแบฃn" + started: "ฤang bแบฏt ฤ‘แบงu xรณa tร i khoแบฃn." + inProgress: "ฤang xรณa dแบงn tร i khoแบฃn." +_ad: + back: "Quay lแบกi" + reduceFrequencyOfThisAd: "Hiแป‡n รญt lแบกi" +_forgotPassword: + enterEmail: "Nhแบญp ฤ‘แป‹a chแป‰ email bแบกn ฤ‘รฃ sแปญ dแปฅng ฤ‘แปƒ ฤ‘ฤƒng kรฝ. Mแป™t liรชn kแบฟt mร  bแบกn cรณ thแปƒ ฤ‘แบทt lแบกi mแบญt khแบฉu cแปงa mรฌnh sau ฤ‘รณ sแบฝ ฤ‘ฦฐแปฃc gแปญi ฤ‘แบฟn nรณ." + ifNoEmail: "Nแบฟu bแบกn khรดng sแปญ dแปฅng email lรบc ฤ‘ฤƒng kรฝ, vui lรฒng liรชn hแป‡ vแป›i quแบฃn trแป‹ viรชn." + contactAdmin: "Mรกy chแปง nร y khรดng hแป— trแปฃ sแปญ dแปฅng ฤ‘แป‹a chแป‰ email, vui lรฒng liรชn hแป‡ vแป›i quแบฃn trแป‹ viรชn ฤ‘แปƒ ฤ‘แบทt lแบกi mแบญt khแบฉu cแปงa bแบกn." +_gallery: + my: "Kho แบขnh" + liked: "Tรบt ฤรฃ Thรญch" + like: "Thรญch" + unlike: "Bแป thรญch" +_email: + _follow: + title: "ฤ‘รฃ theo dรตi bแบกn" + _receiveFollowRequest: + title: "Chแบฅp nhแบญn yรชu cแบงu theo dรตi" +_plugin: + install: "Cร i ฤ‘แบทt tiแป‡n รญch" + installWarn: "Vui lรฒng khรดng cร i ฤ‘แบทt nhแปฏng tiแป‡n รญch ฤ‘รกng ngแป." + manage: "Quแบฃn lรฝ plugin" +_registry: + scope: "Phแบกm vi" + key: "Mรฃ" + keys: "Cรกc mรฃ" + domain: "Tรชn miแปn" + createKey: "Tแบกo mรฃ" +_aboutMisskey: + about: "Misskey lร  phแบงn mแปm mรฃ nguแป“n mแปŸ ฤ‘ฦฐแปฃc phรกt triแปƒn bแปŸi syuilo tแปซ nฤƒm 2014." + contributors: "Nhแปฏng ngฦฐแปi ฤ‘รณng gรณp nแป•i bแบญt" + allContributors: "Toร n bแป™ ngฦฐแปi ฤ‘รณng gรณp" + source: "Mรฃ nguแป“n" + translation: "Dแป‹ch Misskey" + donate: "แปฆng hแป™ Misskey" + morePatrons: "Chรบng tรดi cลฉng trรขn trแปng sแปฑ hแป— trแปฃ cแปงa nhiแปu ngฦฐแปi ฤ‘รณng gรณp khรกc khรดng ฤ‘ฦฐแปฃc liแป‡t kรช แปŸ ฤ‘รขy. Cแบฃm ฦกn! ๐Ÿฅฐ" + patrons: "Ngฦฐแปi แปงng hแป™" +_nsfw: + respect: "แบจn nแป™i dung NSFW" + ignore: "Hiแป‡n nแป™i dung NSFW" + force: "แบจn mแปi media" _mfm: + cheatSheet: "MFM Cheatsheet" + intro: "MFM lร  ngรดn ngแปฏ phรกt triแปƒn ฤ‘แป™c quyแปn cแปงa Misskey cรณ thแปƒ ฤ‘ฦฐแปฃc sแปญ dแปฅng แปŸ nhiแปu nฦกi. Tแบกi ฤ‘รขy bแบกn cรณ thแปƒ xem danh sรกch tแบฅt cแบฃ cรกc cรบ phรกp MFM cรณ sแบตn." + dummy: "Misskey mแปŸ rแป™ng thแบฟ giแป›i Fediverse" + mention: "Nhแบฏc ฤ‘แบฟn" + mentionDescription: "Bแบกn cรณ thแปƒ nhแบฏc ฤ‘แบฟn ai ฤ‘รณ bแบฑng cรกch sแปญ dแปฅng @tรชn ngฦฐแปi dรนng." + hashtag: "Hashtag" + hashtagDescription: "Bแบกn cรณ thแปƒ tแบกo mแป™t hashtag bแบฑng #chแปฏ hoแบทc #sแป‘." + url: "URL" + urlDescription: "Nhแปฏng URL cรณ thแปƒ hiแปƒn thแป‹." + link: "ฤฦฐแปng dแบซn" + linkDescription: "Cรกc phแบงn cแปฅ thแปƒ cแปงa vฤƒn bแบฃn cรณ thแปƒ ฤ‘ฦฐแปฃc hiแปƒn thแป‹ dฦฐแป›i dแบกng URL." + bold: "In ฤ‘แบญm" + boldDescription: "Nแป•i bแบญt cรกc chแปฏ cรกi bแบฑng cรกch lร m chรบng dร y hฦกn." + small: "Nhแป" + smallDescription: "Hiแปƒn thแป‹ nแป™i dung nhแป vร  mแปng." + center: "Giแปฏa" + centerDescription: "Hiแปƒn thแป‹ nแป™i dung cฤƒn giแปฏa." + inlineCode: "Mรฃ (Trong dรฒng)" + inlineCodeDescription: "Hiแปƒn thแป‹ tรด sรกng cรบ phรกp trong dรฒng cho mรฃ (chฦฐฦกng trรฌnh)." + blockCode: "Mรฃ (Khแป‘i)" + blockCodeDescription: "Hiแปƒn thแป‹ tรด sรกng cรบ phรกp cho mรฃ nhiแปu dรฒng (chฦฐฦกng trรฌnh) trong mแป™t khแป‘i." + inlineMath: "Toรกn hแปc (Trong dรฒng)" + inlineMathDescription: "Hiแปƒn thแป‹ cรดng thแปฉc toรกn (KaTeX) trong dรฒng" + blockMath: "Toรกn hแปc (Khแป‘i)" + blockMathDescription: "Hiแปƒn thแป‹ cรดng thแปฉc toรกn hแปc nhiแปu dรฒng (KaTeX) trong mแป™t khแป‘i" + quote: "Trรญch dแบซn" + quoteDescription: "Hiแปƒn thแป‹ nแป™i dung dแบกng lแปi trรญch dแบกng." + emoji: "Tรนy chแป‰nh emoji" + emojiDescription: "Hiแปƒn thแป‹ emoji vแป›i cรบ phรกp :tรชn emoji:" search: "Tรฌm kiแบฟm" + searchDescription: "Hiแปƒn thแป‹ hแป™p tรฌm kiแบฟm vแป›i vฤƒn bแบฃn ฤ‘ฦฐแปฃc nhแบญp trฦฐแป›c." + flip: "Lแบญt" + flipDescription: "Lแบญt nแป™i dung theo chiแปu ngang hoแบทc chiแปu dแปc." + jelly: "Chuyแปƒn ฤ‘แป™ng (Thแบกch rau cรขu)" + jellyDescription: "Cho phรฉp nแป™i dung chuyแปƒn ฤ‘แป™ng giแป‘ng nhฦฐ thแบกch rau cรขu." + tada: "Chuyแปƒn ฤ‘แป™ng (Tada)" + tadaDescription: "Cho phรฉp nแป™i dung chuyแปƒn ฤ‘แป™ng kiแปƒu \"Tada!\"." + jump: "Chuyแปƒn ฤ‘แป™ng (Nhแบฃy mรบa)" + jumpDescription: "Cho phรฉp nแป™i dung chuyแปƒn ฤ‘แป™ng nhแบฃy nhรณt." + bounce: "Chuyแปƒn ฤ‘แป™ng (Cร  tฦฐng)" + bounceDescription: "Cho phรฉp nแป™i dung chuyแปƒn ฤ‘แป™ng cร  tฦฐng." + shake: "Chuyแปƒn ฤ‘แป™ng (Rung)" + shakeDescription: "Cho phรฉp nแป™i dung chuyแปƒn ฤ‘แป™ng rung lแบฏc." + twitch: "Chuyแปƒn ฤ‘แป™ng (Co rรบt)" + twitchDescription: "Cho phรฉp nแป™i dung chuyแปƒn ฤ‘แป™ng co rรบt." + spin: "Chuyแปƒn ฤ‘แป™ng (Xoay tรญt)" + spinDescription: "Cho phรฉp nแป™i dung chuyแปƒn ฤ‘แป™ng xoay tรญt." + x2: "Lฦกฬn" + x2Description: "Hiแปƒn thแป‹ nแป™i dung cแปก lแป›n hฦกn." + x3: "Rแบฅt lแป›n" + x3Description: "Hiแปƒn thแป‹ nแป™i dung cแปก lแป›n hฦกn nแปฏa." + x4: "Khแป•ng lแป“" + x4Description: "Hiแปƒn thแป‹ nแป™i dung cแปก khแป•ng lแป“." + blur: "Lร m mแป" + blurDescription: "Lร m mแป nแป™i dung. Nรณ sแบฝ ฤ‘ฦฐแปฃc hiแปƒn thแป‹ rรต rร ng khi di chuแป™t qua." + font: "Phรดng chแปฏ" + fontDescription: "Chแปn phรดng chแปฏ ฤ‘แปƒ hiแปƒn thแป‹ nแป™i dung." + rainbow: "Cแบงu vแป“ng" + rainbowDescription: "Lร m cho nแป™i dung hiแปƒn thแป‹ vแป›i mร u sแบฏc cแบงu vแป“ng." + sparkle: "Lแบฅp lรกnh" + sparkleDescription: "Lร m cho nแป™i dung hiแป‡u แปฉng hแบกt lแบฅp lรกnh." + rotate: "Xoay" + rotateDescription: "Xoay nแป™i dung theo mแป™t gรณc cแปฅ thแปƒ." +_instanceTicker: + none: "Khรดng hiแปƒn thแป‹" + remote: "Hiแป‡n cho ngฦฐแปi dรนng tแปซ mรกy chแปง khรกc" + always: "Luรดn hiแป‡n" +_serverDisconnectedBehavior: + reload: "Tแปฑ ฤ‘แป™ng tแบฃi lแบกi" + dialog: "Hiแป‡n hแป™p thoแบกi cแบฃnh bรกo" + quiet: "Hiแปƒn thแป‹ cแบฃnh bรกo khรดng phรด trฦฐฦกng" +_channel: + create: "Tแบกo kรชnh" + edit: "Chแป‰nh sแปญa kรชnh" + setBanner: "ฤแบทt แบฃnh bรฌa" + removeBanner: "Xรณa แบฃnh bรฌa" + featured: "Xu hฦฐฦกฬng" + owned: "Do tรดi quแบฃn lรฝ" + following: "ฤang theo dรตi" + usersCount: "{n} Thร nh viรชn" + notesCount: "{n} Tรบt" +_menuDisplay: + sideFull: "Thanh bรชn" + sideIcon: "Thanh bรชn (Biแปƒu tฦฐแปฃng)" + top: "Trรชn cรนng" + hide: "แบจn" +_wordMute: + muteWords: "แบจn tแปซ ngแปฏ" + muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." + muteWordsDescription2: "Bao quanh cรกc tแปซ khรณa bแบฑng dแบฅu gแบกch chรฉo ฤ‘แปƒ sแปญ dแปฅng cแปฅm tแปซ thรดng dแปฅng." + softDescription: "แบจn cรกc tรบt phรน hแปฃp ฤ‘iแปu kiแป‡n ฤ‘รฃ ฤ‘แบทt khแปi bแบฃng tin." + hardDescription: "Ngฤƒn cรกc tรบt ฤ‘รกp แปฉng cรกc ฤ‘iแปu kiแป‡n ฤ‘รฃ ฤ‘แบทt xuแบฅt hiแป‡n trรชn bแบฃng tin. Lฦฐu รฝ, nhแปฏng tรบt nร y sแบฝ khรดng ฤ‘ฦฐแปฃc thรชm vร o bแบฃng tin ngay cแบฃ khi cรกc ฤ‘iแปu kiแป‡n ฤ‘ฦฐแปฃc thay ฤ‘แป•i." + soft: "Yแบฟu" + hard: "Mแบกnh" + mutedNotes: "Nhแปฏng tรบt ฤ‘รฃ แบฉn" +_instanceMute: + instanceMuteDescription: "Thao tรกc nร y sแบฝ แบฉn mแปi tรบt/lฦฐแปฃt ฤ‘ฤƒng lแบกi tแปซ cรกc mรกy chแปง ฤ‘ฦฐแปฃc liแป‡t kรช, bao gแป“m cแบฃ nhแปฏng tรบt dแบกng trแบฃ lแปi tแปซ mรกy chแปง bแป‹ แบฉn." + instanceMuteDescription2: "Tรกch bแบฑng cรกch xuแป‘ng dรฒng" + title: "แบจn tรบt tแปซ nhแปฏng mรกy chแปง ฤ‘รฃ liแป‡t kรช." + heading: "Danh sรกch nhแปฏng mรกy chแปง bแป‹ แบฉn" _theme: + explore: "Khรกm phรก theme" + install: "Cร i ฤ‘แบทt theme" + manage: "Quแบฃn lรฝ theme" + code: "Mรฃ theme" + description: "Mรด tแบฃ" installed: "{name} ฤ‘รฃ ฤ‘ฦฐแปฃc cร i ฤ‘แบทt" + installedThemes: "Theme ฤ‘รฃ cร i ฤ‘แบทt" + builtinThemes: "Theme tรญch hแปฃp sแบตn" + alreadyInstalled: "Theme nร y ฤ‘รฃ ฤ‘ฦฐแปฃc cร i ฤ‘แบทt" + invalid: "ฤแป‹nh dแบกng cแปงa theme nร y khรดng hแปฃp lแป‡" + make: "Tแบกo theme" + base: "Dแปฑa trรชn cรณ sแบตn" + addConstant: "Thรชm hแบฑng sแป‘" + constant: "Hแบฑng sแป‘" + defaultValue: "Giรก trแป‹ mแบทc ฤ‘แป‹nh" + color: "Mร u sแบฏc" + refProp: "Tham chiแบฟu mแป™t thuแป™c tรญnh" + refConst: "Tham chiแบฟu mแป™t hแบฑng sแป‘" + key: "Khรณa" + func: "Hร m" + funcKind: "Loแบกi hร m" + argument: "Tham sแป‘" + basedProp: "Thuแป™c tรญnh tham chiแบฟu" + alpha: "ฤแป™ trong suแป‘t" + darken: "ฤแป™ tแป‘i" + lighten: "ฤรดฬฃ saฬng" + inputConstantName: "Nhแบญp tรชn cho hแบฑng sแป‘ nร y" + importInfo: "Nแบฟu bแบกn nhแบญp mรฃ theme แปŸ ฤ‘รขy, bแบกn cรณ thแปƒ nhแบญp mรฃ ฤ‘รณ vร o trรฌnh chแป‰nh sแปญa theme" + deleteConstantConfirm: "Bแบกn cรณ chแบฏc muแป‘n xรณa hแบฑng sแป‘ {const} khรดng?" + keys: + accent: "Mร u phแปฅ" + bg: "Mร u nแปn" + fg: "Mร u chแปฏ" + focus: "Trแปng tรขm" + indicator: "Chแป‰ bรกo" + panel: "Thanh bรชn" + shadow: "Bรณng mแป" + header: "แบขnh bรฌa" + navBg: "Nแปn thanh bรชn" + navFg: "Chแปฏ thanh bรชn" + navHoverFg: "Chแปฏ thanh bรชn (Khi chแบกm)" + navActive: "Chแปฏ thanh bรชn (Khi chแปn)" + navIndicator: "Chแป‰ bรกo thanh bรชn" + link: "ฤฦฐแปng dแบซn" + hashtag: "Hashtag" + mention: "Nhแบฏc ฤ‘แบฟn" + mentionMe: "Lฦฐแปฃt nhแบฏc (Tรดi)" + renote: "ฤฤƒng lแบกi" + modalBg: "Nแปn phฦฐฦกng thแปฉc" + divider: "Phรขn chia" + scrollbarHandle: "Thanh cuแป™n khi giแปฏ" + scrollbarHandleHover: "Thanh cuแป™n khi chแบกm" + dateLabelFg: "Mร u ngร y thรกng nฤƒm" + infoBg: "Nแปn thรดng tin" + infoFg: "Chแปฏ thรดng tin" + infoWarnBg: "Nแปn cแบฃnh bรกo" + infoWarnFg: "Chแปฏ cแบฃnh bรกo" + cwBg: "Nแปn nรบt nแป™i dung แบฉn" + cwFg: "Chแปฏ nรบt nแป™i dung แบฉn" + cwHoverBg: "Nแปn nรบt nแป™i dung แบฉn (Chแบกm)" + toastBg: "Nแปn thรดng bรกo" + toastFg: "Chแปฏ thรดng bรกo" + buttonBg: "Nแปn nรบt" + buttonHoverBg: "Nแปn nรบt (Chแบกm)" + inputBorder: "ฤฦฐแปng viแปn khung soแบกn thแบฃo" + listItemHoverBg: "Nแปn mแปฅc liแป‡t kรช (Chแบกm)" + driveFolderBg: "Nแปn thฦฐ mแปฅc แป” ฤ‘ฤฉa" + wallpaperOverlay: "Lแป›p phแปง hรฌnh nแปn" + badge: "Huy hiแป‡u" + messageBg: "Nแปn chat" + accentDarken: "Mร u phแปฅ (Tแป‘i)" + accentLighten: "Mร u phแปฅ (Sรกng)" + fgHighlighted: "Chแปฏ nแป•i bแบญt" _sfx: + note: "Tรบt" + noteMy: "Tรบt cแปงa tรดi" notification: "Thรดng bรกo" + chat: "Trรฒ chuyแป‡n" + chatBg: "Chat (Nแปn)" + antenna: "Trแบกm phรกt sรณng" + channel: "Kรชnh" _ago: - unknown: "Khรดng rรต" future: "Tฦฐฦกng lai" justNow: "Vแปซa xong" secondsAgo: "{n}s trฦฐแป›c" @@ -39,10 +1097,563 @@ _ago: weeksAgo: "{n} tuแบงn trฦฐแป›c" monthsAgo: "{n} thรกng trฦฐแป›c" yearsAgo: "{n} nฤƒm trฦฐแป›c" +_time: + second: "s" + minute: "phรบt" + hour: "giแป" + day: "ngร y" +_tutorial: + title: "Cรกch dรนng Misskey" + step1_1: "Xin chร o!" + step1_2: "Trang nร y gแปi lร  \"bแบฃng tin\". Nรณ hiแป‡n \"tรบt\" tแปซ nhแปฏng ngฦฐแปi mร  bแบกn \"theo dรตi\" theo thแปฉ tแปฑ thแปi gian." + step1_3: "Bแบฃng tin cแปงa bแบกn ฤ‘ang trแป‘ng, bแปŸi vรฌ bแบกn chฦฐa ฤ‘ฤƒng tรบt nร o hoแบทc chฦฐa theo dรตi ai." + step2_1: "Hรฃy hoร n thร nh viแป‡c thiแบฟt lแบญp hแป“ sฦก cแปงa bแบกn trฦฐแป›c khi viแบฟt tรบt hoแบทc theo dรตi bแบฅt kแปณ ai." + step2_2: "Cung cแบฅp mแป™t sแป‘ thรดng tin giแป›i thiแป‡u bแบกn lร  ai sแบฝ giรบp ngฦฐแปi khรกc dแป… dร ng biแบฟt ฤ‘ฦฐแปฃc hแป muแป‘n ฤ‘แปc tรบt hay theo dรตi bแบกn." + step3_1: "Hoร n thร nh thiแบฟt lแบญp hแป“ sฦก cแปงa bแบกn?" + step3_2: "Sau ฤ‘รณ, hรฃy thแปญ ฤ‘ฤƒng mแป™t tรบt tiแบฟp theo. Bแบกn cรณ thแปƒ lร m nhฦฐ vแบญy bแบฑng cรกch nhแบฅn vร o nรบt cรณ biแปƒu tฦฐแปฃng bรบt chรฌ trรชn mร n hรฌnh." + step3_3: "Nhแบญp nแป™i dung vร o khung soแบกn thแบฃo vร  nhแบฅn nรบt ฤ‘ฤƒng แปŸ gรณc trรชn." + step3_4: "Chฦฐa biแบฟt nรณi gรฌ? Thแปญ \"Tรดi mแป›i tham gia Misskey\"!" + step4_1: "ฤฤƒng xong tรบt ฤ‘แบงu tiรชn cแปงa bแบกn?" + step4_2: "De! Tรบt ฤ‘แบงu tiรชn cแปงa bแบกn ฤ‘รฃ hiแป‡n trรชn bแบฃng tin." + step5_1: "Bรขy giแป, hรฃy thแปญ lร m cho bแบฃng tin cแปงa bแบกn sinh ฤ‘แป™ng hฦกn bแบฑng cรกch theo dรตi nhแปฏng ngฦฐแปi khรกc." + step5_2: "{feature} sแบฝ hiแปƒn thแป‹ cho bแบกn cรกc tรบt nแป•i bแบญt trรชn mรกy chแปง nร y. {explore} sแบฝ cho phรฉp bแบกn tรฌm thแบฅy nhแปฏng ngฦฐแปi dรนng thรบ vแป‹. Hรฃy thแปญ tรฌm nhแปฏng ngฦฐแปi bแบกn muแป‘n theo dรตi แปŸ ฤ‘รณ!" + step5_3: "ฤแปƒ theo dรตi nhแปฏng ngฦฐแปi dรนng khรกc, hรฃy nhแบฅn vร o แบฃnh ฤ‘แบกi diแป‡n cแปงa hแป vร  nhแบฅn nรบt \"Theo dรตi\" trรชn hแป“ sฦก cแปงa hแป." + step5_4: "Nแบฟu ngฦฐแปi dรนng khรกc cรณ biแปƒu tฦฐแปฃng แป• khรณa bรชn cแบกnh tรชn cแปงa hแป, cรณ thแปƒ mแบฅt mแป™t khoแบฃng thแปi gian ฤ‘แปƒ ngฦฐแปi dรนng ฤ‘รณ phรช duyแป‡t yรชu cแบงu theo dรตi cแปงa bแบกn theo cรกch thแปง cรดng." + step6_1: "Bแบกn sแบฝ cรณ thแปƒ xem tรบt cแปงa nhแปฏng ngฦฐแปi dรนng khรกc trรชn bแบฃng tin cแปงa mรฌnh ngay bรขy giแป." + step6_2: "Bแบกn cลฉng cรณ thแปƒ ฤ‘แบทt \"biแปƒu cแบฃm\" trรชn tรบt cแปงa ngฦฐแปi khรกc ฤ‘แปƒ phแบฃn hแป“i nhanh chรบng." + step6_3: "ฤแปƒ ฤ‘รญnh kรจm \"biแปƒu cแบฃm\", hรฃy nhแบฅn vร o dแบฅu \"+\" trรชn tรบt cแปงa ngฦฐแปi dรนng khรกc rแป“i chแปn biแปƒu tฦฐแปฃng cแบฃm xรบc mร  bแบกn muแป‘n dรนng." + step7_1: "Xin chรบc mแปซng! Bรขy giแป bแบกn ฤ‘รฃ hoร n thร nh phแบงn hฦฐแป›ng dแบซn cฦก bแบฃn cแปงa Misskey." + step7_2: "Nแบฟu bแบกn muแป‘n tรฌm hiแปƒu thรชm vแป Misskey, hรฃy thแปญ phแบงn {help}." + step7_3: "Bรขy giแป, chรบc may mแบฏn vร  vui vแบป vแป›i Misskey! ๐Ÿš€" +_2fa: + alreadyRegistered: "Bแบกn ฤ‘รฃ ฤ‘ฤƒng kรฝ thiแบฟt bแป‹ xรกc minh 2 bฦฐแป›c." + registerDevice: "ฤฤƒng kรฝ mแป™t thiแบฟt bแป‹" + registerKey: "ฤฤƒng kรฝ mแป™t mรฃ bแบฃo vแป‡" + step1: "Trฦฐแป›c tiรชn, hรฃy cร i ฤ‘แบทt mแป™t แปฉng dแปฅng xรกc minh (chแบณng hแบกn nhฦฐ {a} hoแบทc {b}) trรชn thiแบฟt bแป‹ cแปงa bแบกn." + step2: "Sau ฤ‘รณ, quรฉt mรฃ QR hiแปƒn thแป‹ trรชn mร n hรฌnh nร y." + step2Url: "Bแบกn cลฉng cรณ thแปƒ nhแบญp URL nร y nแบฟu sแปญ dแปฅng mแป™t chฦฐฦกng trรฌnh mรกy tรญnh:" + step3: "Nhแบญp mรฃ token do แปฉng dแปฅng cแปงa bแบกn cung cแบฅp ฤ‘แปƒ hoร n tแบฅt thiแบฟt lแบญp." + step4: "Kแปƒ tแปซ bรขy giแป, nhแปฏng lแบงn ฤ‘ฤƒng nhแบญp trong tฦฐฦกng lai sแบฝ yรชu cแบงu mรฃ token ฤ‘ฤƒng nhแบญp ฤ‘รณ." + securityKeyInfo: "Bรชn cแบกnh xรกc minh bแบฑng vรขn tay hoแบทc mรฃ PIN, bแบกn cลฉng cรณ thแปƒ thiแบฟt lแบญp xรกc minh thรดng qua khรณa bแบฃo mแบญt phแบงn cแปฉng hแป— trแปฃ FIDO2 ฤ‘แปƒ bแบฃo mแบญt hฦกn nแปฏa cho tร i khoแบฃn cแปงa mรฌnh." +_permissions: + "read:account": "Xem thรดng tin tร i khoแบฃn cแปงa bแบกn" + "write:account": "Sแปญa thรดng tin tร i khoแบฃn cแปงa bแบกn" + "read:blocks": "Xem danh sรกch ngฦฐแปi bแบกn chแบทn" + "write:blocks": "Sแปญa danh sรกch ngฦฐแปi bแบกn chแบทn" + "read:drive": "Truy cแบญp tแบญp tin, thฦฐ mแปฅc trong แป” ฤ‘ฤฉa" + "write:drive": "Sแปญa vร  xรณa tแบญp tin, thฦฐ mแปฅc trong แป” ฤ‘ฤฉa" + "read:favorites": "Xem lฦฐแปฃt thรญch cแปงa tรดi" + "write:favorites": "Sแปญa lฦฐแปฃt thรญch cแปงa tรดi" + "read:following": "Xem nhแปฏng ngฦฐแปi bแบกn theo dรตi" + "write:following": "Theo dรตi hoแบทc ngฦฐng theo dรตi ai ฤ‘รณ" + "read:messaging": "Xem lแป‹ch sแปญ chat" + "write:messaging": "Soแบกn hoแบทc xรณa tin nhแบฏn" + "read:mutes": "Xem nhแปฏng ngฦฐแปi bแบกn แบฉn" + "write:mutes": "Sแปญa nhแปฏng ngฦฐแปi bแบกn แบฉn" + "write:notes": "Soแบกn hoแบทc xรณa tรบt" + "read:notifications": "Xem thรดng bรกo cแปงa tรดi" + "write:notifications": "Quแบฃn lรฝ thรดng bรกo cแปงa tรดi" + "read:reactions": "Xem lฦฐแปฃt biแปƒu cแบฃm cแปงa tรดi" + "write:reactions": "Sแปญa lฦฐแปฃt biแปƒu cแบฃm cแปงa tรดi" + "write:votes": "Bรฌnh chแปn" + "read:pages": "Xem trang cแปงa tรดi" + "write:pages": "Sแปญa hoแบทc xรณa trang cแปงa tรดi" + "read:page-likes": "Xem lฦฐแปฃt thรญch trรชn trang cแปงa tรดi" + "write:page-likes": "Sแปญa lฦฐแปฃt thรญch cแปงa tรดi trรชn trang" + "read:user-groups": "Xem nhรณm cแปงa tรดi" + "write:user-groups": "Sแปญa hoแบทc xรณa nhรณm cแปงa tรดi" + "read:channels": "Xem kรชnh cแปงa tรดi" + "write:channels": "Sแปญa kรชnh cแปงa tรดi" + "read:gallery": "Xem kho แบฃnh cแปงa tรดi" + "write:gallery": "Sแปญa kho แบฃnh cแปงa tรดi" + "read:gallery-likes": "Xem danh sรกch cรกc tรบt ฤ‘รฃ thรญch trong thฦฐ viแป‡n cแปงa tรดi" + "write:gallery-likes": "Sแปญa danh sรกch cรกc tรบt ฤ‘รฃ thรญch trong thฦฐ viแป‡n cแปงa tรดi" +_auth: + shareAccess: "Bแบกn cรณ muแป‘n cho phรฉp \"{name}\" truy cแบญp vร o tร i khoแบฃn nร y khรดng?" + shareAccessAsk: "Bแบกn cรณ chแบฏc muแป‘n cho phรฉp แปฉng dแปฅng nร y truy cแบญp vร o tร i khoแบฃn cแปงa mรฌnh khรดng?" + permissionAsk: "แปจng dแปฅng nร y yรชu cแบงu cรกc quyแปn sau" + pleaseGoBack: "Vui lรฒng quay lแบกi แปฉng dแปฅng" + callback: "Quay lแบกi แปฉng dแปฅng" + denied: "Truy cแบญp bแป‹ tแปซ chแป‘i" +_antennaSources: + all: "Toร n bแป™ tรบt" + homeTimeline: "Tรบt tแปซ nhแปฏng ngฦฐแปi ฤ‘รฃ theo dรตi" + users: "Tรบt tแปซ nhแปฏng ngฦฐแปi cแปฅ thแปƒ" + userList: "Tรบt tแปซ danh sรกch ngฦฐแปi dรนng cแปฅ thแปƒ" + userGroup: "Tรบt tแปซ ngฦฐแปi dรนng trong mแป™t nhรณm cแปฅ thแปƒ" +_weekday: + sunday: "Chแปง Nhแบญt" + monday: "Thแปฉ Hai" + tuesday: "Thแปฉ Ba" + wednesday: "Thแปฉ Tฦฐ" + thursday: "Thแปฉ Nฤƒm" + friday: "Thแปฉ Sรกu" + saturday: "Thแปฉ Bแบฃy" _widgets: + memo: "Tรบt ฤ‘รฃ ghim" notifications: "Thรดng bรกo" + timeline: "Bแบฃng tin" + calendar: "Lแป‹ch" + trends: "Xu hฦฐฦกฬng" + clock: "ฤแป“ng hแป“" + rss: "Trรฌnh ฤ‘แปc RSS" + activity: "Hoแบกt ฤ‘แป™ng" + photos: "Kho แบฃnh" + digitalClock: "ฤแป“ng hแป“ sแป‘" + federation: "Liรชn hแปฃp" + postForm: "Mแบซu ฤ‘ฤƒng" + slideshow: "Trรฌnh chiแบฟu" + button: "Nรบt" + onlineUsers: "Ai ฤ‘ang online" + jobQueue: "Cรดng viแป‡c chแป xแปญ lรฝ" + serverMetric: "Thแป‘ng kรช mรกy chแปง" + aiscript: "AiScript console" + aichan: "Ai" +_cw: + hide: "แบจn" + show: "Tแบฃi thรชm" + chars: "{count} kรฝ tแปฑ" + files: "{count} tแบญp tin" +_poll: + noOnlyOneChoice: "Cแบงn รญt nhแบฅt hai lแปฑa chแปn." + choiceN: "Lแปฑa chแปn {n}" + noMore: "Bแบกn khรดng thแปƒ thรชm lแปฑa chแปn" + canMultipleVote: "Cho phรฉp chแปn nhiแปu lแปฑa chแปn" + expiration: "Thแปi hแบกn" + infinite: "Vฤฉnh viแป…n" + at: "Kแบฟt thรบc vร o..." + after: "Kแบฟt thรบc sau..." + deadlineDate: "Ngร y kแบฟt thรบc" + deadlineTime: "giแป" + duration: "Thแปi hแบกn" + votesCount: "{n} bรฌnh chแปn" + totalVotes: "{n} tแป•ng bรฌnh chแปn" + vote: "Bรฌnh chแปn" + showResult: "Xem kแบฟt quแบฃ" + voted: "ฤรฃ bรฌnh chแปn" + closed: "ฤรฃ kแบฟt thรบc" + remainingDays: "{d} ngร y {h} giแป cรฒn lแบกi" + remainingHours: "{h} giแป {m} phรบt cรฒn lแบกi" + remainingMinutes: "{m} phรบt {s}s cรฒn lแบกi" + remainingSeconds: "{s}s cรฒn lแบกi" +_visibility: + public: "Cรดng khai" + publicDescription: "Mแปi ngฦฐแปi ฤ‘แปu cรณ thแปƒ ฤ‘แปc tรบt cแปงa bแบกn" + home: "Trang chรญnh" + homeDescription: "Chแป‰ ฤ‘ฤƒng lรชn bแบฃng tin nhร " + followers: "Ngฦฐแปi theo dรตi" + followersDescription: "Dร nh riรชng cho ngฦฐแปi theo dรตi" + specified: "Nhแบฏn riรชng" + specifiedDescription: "Chแป‰ ngฦฐแปi ฤ‘ฦฐแปฃc nhแบฏc ฤ‘แบฟn mแป›i thแบฅy" + localOnly: "Chแป‰ trรชn mรกy chแปง" + localOnlyDescription: "Khรดng hiแปƒn thแป‹ vแป›i ngฦฐแปi แปŸ mรกy chแปง khรกc" +_postForm: + replyPlaceholder: "Trแบฃ lแปi tรบt nร y" + quotePlaceholder: "Trรญch dแบซn tรบt nร y" + channelPlaceholder: "ฤฤƒng lรชn mแป™t kรชnh" + _placeholders: + a: "Bแบกn ฤ‘ang ฤ‘แป‹nh lร m gรฌ?" + b: "Hรดm nay bแบกn cรณ gรฌ vui?" + c: "Bแบกn ฤ‘ang nghฤฉ gรฌ?" + d: "Bแบกn muแป‘n nรณi gรฌ?" + e: "Bแบฏt ฤ‘แบงu viแบฟt..." + f: "ฤang chแป bแบกn viแบฟt..." _profile: + name: "Tรชn" username: "Tรชn ngฦฐแปi dรนng" + description: "Tiแปƒu sแปญ" + youCanIncludeHashtags: "Bแบกn cรณ thแปƒ dรนng hashtag trong tiแปƒu sแปญ." + metadata: "Thรดng tin bแป• sung" + metadataEdit: "Sแปญa thรดng tin bแป• sung" + metadataDescription: "Sแปญ dแปฅng phแบงn nร y, bแบกn cรณ thแปƒ hiแปƒn thแป‹ cรกc mแปฅc thรดng tin bแป• sung trong hแป“ sฦก cแปงa mรฌnh." + metadataLabel: "Nhรฃn" + metadataContent: "Nแป™i dung" + changeAvatar: "ฤแป•i แบฃnh ฤ‘แบกi diแป‡n" + changeBanner: "ฤแป•i แบฃnh bรฌa" +_exportOrImport: + allNotes: "Toร n bแป™ tรบt" + followingList: "ฤang theo dรตi" + muteList: "แบจn" + blockingList: "Chแบทn" + userLists: "Danh sรกch" + excludeMutingUsers: "Loแบกi trแปซ nhแปฏng ngฦฐแปi dรนng bแป‹ แบฉn" + excludeInactiveUsers: "Loแบกi trแปซ nhแปฏng ngฦฐแปi dรนng khรดng hoแบกt ฤ‘แป™ng" +_charts: + federation: "Liรชn hแปฃp" + apRequest: "Yรชu cแบงu" + usersIncDec: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng ngฦฐแปi dรนng" + usersTotal: "Tแป•ng sแป‘ ngฦฐแปi dรนng" + activeUsers: "Sแป‘ ngฦฐแปi ฤ‘ang hoแบกt ฤ‘แป™ng" + notesIncDec: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng tรบt" + localNotesIncDec: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng tรบt mรกy chแปง nร y" + remoteNotesIncDec: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng tรบt tแปซ mรกy chแปง khรกc" + notesTotal: "Tแป•ng sแป‘ sรบt" + filesIncDec: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng tแบญp tin" + filesTotal: "Tแป•ng sแป‘ tแบญp tin" + storageUsageIncDec: "Sแปฑ khรกc biแป‡t vแป dung lฦฐแปฃng lฦฐu trแปฏ" + storageUsageTotal: "Tแป•ng dung lฦฐแปฃng lฦฐu trแปฏ" +_instanceCharts: + requests: "Lฦฐแปฃt yรชu cแบงu" + users: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng ngฦฐแปi dรนng" + usersTotal: "Sแป‘ lฦฐแปฃng ngฦฐแปi dรนng tรญch lลฉy" + notes: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng tรบt" + notesTotal: "Sแป‘ lฦฐแปฃng tรบt tรญch lลฉy" + ff: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng ngฦฐแปi dรนng ฤ‘ฦฐแปฃc theo dรตi/ngฦฐแปi theo dรตi" + ffTotal: "Sแป‘ lฦฐแปฃng ngฦฐแปi dรนng ฤ‘ฦฐแปฃc theo dรตi/ngฦฐแปi theo dรตi tรญch lลฉy" + cacheSize: "Sแปฑ khรกc biแป‡t vแป dung lฦฐแปฃng bแป™ nhแป› ฤ‘แป‡m" + cacheSizeTotal: "Dung lฦฐแปฃng bแป™ nhแป› ฤ‘แป‡m tรญch lลฉy" + files: "Sแปฑ khรกc biแป‡t vแป sแป‘ lฦฐแปฃng tแบญp tin" + filesTotal: "Sแป‘ lฦฐแปฃng tแบญp tin tรญch lลฉy" +_timelines: + home: "Trang chรญnh" + local: "Mรกy chแปง nร y" + social: "Xรฃ hแป™i" + global: "Liรชn hแปฃp" +_pages: + newPage: "Tแบกo Trang mแป›i" + editPage: "Sแปญa Trang nร y" + readPage: "Xem mรฃ nguแป“n Trang nร y" + created: "Trang ฤ‘รฃ ฤ‘ฦฐแปฃc tแบกo thร nh cรดng" + updated: "Trang ฤ‘รฃ ฤ‘ฦฐแปฃc cแบญp nhแบญt thร nh cรดng" + deleted: "Trang ฤ‘รฃ ฤ‘ฦฐแปฃc xรณa thร nh cรดng" + pageSetting: "Cร i ฤ‘แบทt trang" + nameAlreadyExists: "URL Trang ฤ‘รฃ tแป“n tแบกi" + invalidNameTitle: "URL Trang khรดng hแปฃp lแป‡" + invalidNameText: "Khรดng ฤ‘ฦฐแปฃc ฤ‘แปƒ trแป‘ng tแปฑa ฤ‘แป Trang" + editThisPage: "Sแปญa Trang nร y" + viewSource: "Xem maฬƒ nguรดฬ€n" + viewPage: "Xem trang cแปงa tรดi" + like: "Thรญch" + unlike: "Bแป thรญch" + my: "Trang cแปงa tรดi" + liked: "Trang ฤ‘รฃ thรญch" + featured: "Nแป•i tiแบฟng" + inspector: "Thanh tra" + contents: "Nแป™i dung" + content: "Chแบทn Trang" + variables: "Biแบฟn thแปƒ" + title: "Tแปฑa ฤ‘แป" + url: "URL Trang" + summary: "Mรด tแบฃ Trang" + alignCenter: "Cฤƒn giแปฏa" + hideTitleWhenPinned: "แบจn tแปฑa ฤ‘แป Trang khi ghim lรชn hแป“ sฦก" + font: "Phรดng chแปฏ" + fontSerif: "Serif" + fontSansSerif: "Sans Serif" + eyeCatchingImageSet: "ฤแบทt แบฃnh thu nhแป" + eyeCatchingImageRemove: "Xรณa แบฃnh thu nhแป" + chooseBlock: "Thรชm khแป‘i" + selectType: "Chแปn kiแปƒu" + enterVariableName: "Nhแบญp tรชn mแป™t biแบฟn thแปƒ" + variableNameIsAlreadyUsed: "Tรชn biแบฟn thแปƒ nร y ฤ‘รฃ ฤ‘ฦฐแปฃc sแปญ dแปฅng" + contentBlocks: "Nแป™i dung" + inputBlocks: "Nhแบญp" + specialBlocks: "ฤแบทc biแป‡t" + blocks: + text: "Vฤƒn bแบฃn" + textarea: "Khu vแปฑc vฤƒn bแบฃn" + section: "Mแปฅc " + image: "Hรฌnh แบฃnh" + button: "Nรบt" + if: "Nแบฟu" + _if: + variable: "Biแบฟn thแปƒ" + post: "Mแบซu ฤ‘ฤƒng" + _post: + text: "Nแป™i dung" + attachCanvasImage: "ฤรญnh kรจm hรฌnh canva" + canvasId: "ID Canva" + textInput: "Vฤƒn bแบฃn ฤ‘แบงu vร o" + _textInput: + name: "Tรชn biแบฟn thแปƒ" + text: "Tแปฑa ฤ‘แป" + default: "Giรก trแป‹ mแบทc ฤ‘แป‹nh" + textareaInput: "Vฤƒn bแบฃn nhiแปu dรฒng ฤ‘แบงu vร o" + _textareaInput: + name: "Tรชn biแบฟn thแปƒ" + text: "Tแปฑa ฤ‘แป" + default: "Giรก trแป‹ mแบทc ฤ‘แป‹nh" + numberInput: "ฤแบงu vร o sแป‘" + _numberInput: + name: "Tรชn biแบฟn thแปƒ" + text: "Tแปฑa ฤ‘แป" + default: "Giรก trแป‹ mแบทc ฤ‘แป‹nh" + canvas: "Canva" + _canvas: + id: "ID Canva" + width: "Chiแปu rแป™ng" + height: "Chiแปu cao" + note: "Tรบt ฤ‘รฃ nhรบng" + _note: + id: "ID tรบt" + idDescription: "Ngoร i ra, bแบกn cรณ thแปƒ dรกn URL tรบt vร o ฤ‘รขy." + detailed: "Xem chi tiแบฟt" + switch: "Chuyแปƒn ฤ‘แป•i" + _switch: + name: "Tรชn biแบฟn thแปƒ" + text: "Tแปฑa ฤ‘แป" + default: "Giรก trแป‹ mแบทc ฤ‘แป‹nh" + counter: "Bแป™ ฤ‘แบฟm" + _counter: + name: "Tรชn biแบฟn thแปƒ" + text: "Tแปฑa ฤ‘แป" + inc: "Bฦฐแป›c" + _button: + text: "Tแปฑa ฤ‘แป" + colored: "Vแป›i mร u" + action: "Thao tรกc khi nhแบฅn nรบt" + _action: + dialog: "Hiแป‡n hแป™p thoแบกi" + _dialog: + content: "Nแป™i dung" + resetRandom: "ฤแบทt lแบกi seed ngแบซu nhiรชn" + pushEvent: "Gแปญi mแป™t sแปฑ kiแป‡n" + _pushEvent: + event: "Tรชn sแปฑ kiแป‡n" + message: "Tin nhแบฏn hiแปƒn thแป‹ khi kรญch hoแบกt" + variable: "Biแปƒn thแปƒ ฤ‘แปƒ gแปญi" + no-variable: "Khรดng" + callAiScript: "Gแปi AiScript" + _callAiScript: + functionName: "Tรชn tรญnh nฤƒng" + radioButton: "Lแปฑa chแปn" + _radioButton: + name: "Tรชn biแบฟn thแปƒ" + title: "Tแปฑa ฤ‘แป" + values: "Phรขn tรกch cรกc mแปฅc bแบฑng cรกch xuแป‘ng dรฒng" + default: "Giรก trแป‹ mแบทc ฤ‘แป‹nh" + script: + categories: + flow: "ฤiแปu khiแปƒn" + logical: "Hoแบกt ฤ‘แป™ng logic" + operation: "Tรญnh toรกn" + comparison: "So sรกnh" + random: "Ngแบซu nhiรชn" + value: "Giรก trแป‹" + fn: "Tรญnh nฤƒng" + text: "Tรกc vแปฅ vฤƒn bแบฃn" + convert: "Chuyแปƒn ฤ‘แป•i" + list: "Danh sรกch" + blocks: + text: "Vฤƒn bแบฃn" + multiLineText: "Vฤƒn bแบฃn (nhiแปu dรฒng)" + textList: "Vฤƒn bแบฃn liแป‡t kรช" + _textList: + info: "Phรขn tรกch mแปฅc bแบฑng cรกch xuแป‘ng dรฒng" + strLen: "ฤแป™ dร i vฤƒn bแบฃn" + _strLen: + arg1: "Vฤƒn bแบฃn" + strPick: "Trรญch xuแบฅt chuแป—i" + _strPick: + arg1: "Vฤƒn bแบฃn" + arg2: "Vแป‹ trรญ chuแป—i" + strReplace: "Thay thแบฟ chuแป—i" + _strReplace: + arg1: "Nแป™i dung" + arg2: "Vฤƒn bแบฃn thay thแบฟ" + arg3: "Thay thแบฟ bแบฑng" + strReverse: "Lแบญt vฤƒn bแบฃn" + _strReverse: + arg1: "Vฤƒn bแบฃn" + join: "Nแป‘i vฤƒn bแบฃn" + _join: + arg1: "Danh sรกch" + arg2: "Phรขn cรกch" + add: "Cแป™ng" + _add: + arg1: "A" + arg2: "B" + subtract: "Trแปซ" + _subtract: + arg1: "A" + arg2: "B" + multiply: "Nhรขn" + _multiply: + arg1: "A" + arg2: "B" + divide: "Chia" + _divide: + arg1: "A" + arg2: "B" + mod: "Phแบงn cรฒn lแบกi" + _mod: + arg1: "A" + arg2: "B" + round: "Lร m trรฒn thแบญp phรขn" + _round: + arg1: "Sแป‘" + eq: "A vร  B bแบฑng nhau" + _eq: + arg1: "A" + arg2: "B" + notEq: "A vร  B khรกc nhau" + _notEq: + arg1: "A" + arg2: "B" + and: "A Vร€ B" + _and: + arg1: "A" + arg2: "B" + or: "A HOแบถC B" + _or: + arg1: "A" + arg2: "B" + lt: "< A nhแป hฦกn B" + _lt: + arg1: "A" + arg2: "B" + gt: "> A lแป›n hฦกn B" + _gt: + arg1: "A" + arg2: "B" + ltEq: "<= A nhแป hฦกn hoแบทc bแบฑng B" + _ltEq: + arg1: "A" + arg2: "B" + gtEq: ">= A lแป›n hฦกn hoแบทc bแบฑng B" + _gtEq: + arg1: "A" + arg2: "B" + if: "Nhรกnh" + _if: + arg1: "Nแบฟu" + arg2: "Sau ฤ‘รณ" + arg3: "Khรกc" + not: "KHร”NG" + _not: + arg1: "KHร”NG" + random: "Ngแบซu nhiรชn" + _random: + arg1: "Xรกc suแบฅt" + rannum: "Sแป‘ ngแบซu nhiรชn" + _rannum: + arg1: "Giรก trแป‹ tแป‘i thiแปƒu" + arg2: "Giรก trแป‹ tแป‘i ฤ‘a" + randomPick: "Chแปn ngแบซu nhiรชn tแปซ danh sรกch" + _randomPick: + arg1: "Danh sรกch" + dailyRandom: "Ngแบซu nhiรชn (ฤแป•i mแป—i ngฦฐแปi mแป™t lแบงn mแป—i ngร y)" + _dailyRandom: + arg1: "Xรกc suแบฅt" + dailyRannum: "Sแป‘ ngแบซu nhiรชn (ฤแป•i mแป—i ngฦฐแปi mแป™t lแบงn mแป—i ngร y)" + _dailyRannum: + arg1: "Giรก trแป‹ tแป‘i thiแปƒu" + arg2: "Giรก trแป‹ tแป‘i ฤ‘a" + dailyRandomPick: "Chแปn ngแบซu nhiรชn tแปซ mแป™t danh sรกch (ฤแป•i mแป—i ngฦฐแปi mแป™t lแบงn mแป—i ngร y)" + _dailyRandomPick: + arg1: "Danh sรกch" + seedRandom: "Ngแบซu nhiรชn (vแป›i seed)" + _seedRandom: + arg1: "Seed" + arg2: "Xรกc suแบฅt" + seedRannum: "Sแป‘ ngแบซu nhiรชn (vแป›i seed)" + _seedRannum: + arg1: "Seed" + arg2: "Giรก trแป‹ tแป‘i thiแปƒu" + arg3: "Giรก trแป‹ tแป‘i ฤ‘a" + seedRandomPick: "Chแปn ngแบซu nhiรชn tแปซ danh sรกch (vแป›i seed)" + _seedRandomPick: + arg1: "Seed" + arg2: "Danh sรกch" + DRPWPM: "Chแปn ngแบซu nhiรชn tแปซ danh sรกch nแบทng (ฤแป•i mแป—i ngฦฐแปi mแป™t lแบงn mแป—i ngร y)" + _DRPWPM: + arg1: "Vฤƒn bแบฃn liแป‡t kรช" + pick: "Chแปn tแปซ danh sรกch" + _pick: + arg1: "Danh sรกch" + arg2: "Vแป‹ trรญ" + listLen: "Lแบฅy ฤ‘แป™ dร i danh sรกch" + _listLen: + arg1: "Danh sรกch" + number: "Sแป‘" + stringToNumber: "Chแปฏ thร nh sแป‘" + _stringToNumber: + arg1: "Vฤƒn bแบฃn" + numberToString: "Sแป‘ thร nh chแปฏ" + _numberToString: + arg1: "Sแป‘" + splitStrByLine: "Phรขn cรกch vฤƒn bแบฃn bแบฑng cรกch xuแป‘ng dรฒng" + _splitStrByLine: + arg1: "Vฤƒn bแบฃn" + ref: "Biแบฟn thแปƒ" + aiScriptVar: "Biแปƒn thแปƒ AiScript" + fn: "Tรญnh nฤƒng" + _fn: + slots: "Chแป—" + slots-info: "Phรขn cรกch chแป— bแบฑng cรกch xuแป‘ng dรฒng" + arg1: "ฤแบงu ra" + for: "ฤ‘แปƒ-Lแบทp lแบกi" + _for: + arg1: "Sแป‘ lแบงn lแบทp lแบกi" + arg2: "Hร nh ฤ‘แป™ng" + typeError: "Chแป— {slot} chแบฅp nhแบญn cรกc giรก trแป‹ thuแป™c loแบกi \"{expect}\", nhฦฐng giรก trแป‹ ฤ‘ฦฐแปฃc cung cแบฅp thuแป™c loแบกi \"{actual}\"!" + thereIsEmptySlot: "Chแป— {slot} ฤ‘ang trแป‘ng!" + types: + string: "Vฤƒn bแบฃn" + number: "Sแป‘" + boolean: "Cแป" + array: "Danh sรกch" + stringArray: "Vฤƒn bแบฃn liแป‡t kรช" + emptySlot: "Chแป— trแป‘ng" + enviromentVariables: "Biแบฟn mรดi trฦฐแปng" + pageVariables: "Biแบฟn trang" + argVariables: "ฤแบงu vร o chแป—" +_relayStatus: + requesting: "ฤang chแป" + accepted: "ฤรฃ duyแป‡t" + rejected: "ฤรฃ tแปซ chแป‘i" +_notification: + fileUploaded: "ฤรฃ tแบฃi lรชn tแบญp tin" + youGotMention: "{name} nhแบฏc ฤ‘แบฟn bแบกn" + youGotReply: "{name} trแบฃ lแปi bแบกn" + youGotQuote: "{name} trรญch dแบซn tรบt cแปงa bแบกn" + youRenoted: "{name} ฤ‘ฤƒng lแบกi tรบt cแปงa bแบกn" + youGotPoll: "{name} bรฌnh chแปn tรบt cแปงa bแบกn" + youGotMessagingMessageFromUser: "{name} nhแบฏn tin cho bแบกn" + youGotMessagingMessageFromGroup: "Mแป™t tin nhแบฏn trong nhรณm {name}" + youWereFollowed: "ฤ‘รฃ theo dรตi bแบกn" + youReceivedFollowRequest: "Bแบกn vแปซa cรณ mแป™t yรชu cแบงu theo dรตi" + yourFollowRequestAccepted: "Yรชu cแบงu theo dรตi cแปงa bแบกn ฤ‘รฃ ฤ‘ฦฐแปฃc chแบฅp nhแบญn" + youWereInvitedToGroup: "Bแบกn ฤ‘รฃ ฤ‘ฦฐแปฃc mแปi tham gia nhรณm" + pollEnded: "Cuแป™c bรฌnh chแปn ฤ‘รฃ kแบฟt thรบc" + emptyPushNotificationMessage: "ฤรฃ cแบญp nhแบญt thรดng bรกo ฤ‘แบฉy" + _types: + all: "Toร n bแป™" + follow: "ฤang theo dรตi" + mention: "Nhแบฏc ฤ‘แบฟn" + reply: "Lฦฐแปฃt trแบฃ lแปi" + renote: "ฤฤƒng lแบกi" + quote: "Trรญch dแบซn" + reaction: "Biแปƒu cแบฃm" + pollVote: "Lฦฐแปฃt bรฌnh chแปn" + pollEnded: "Bรฌnh chแปn kแบฟt thรบc" + receiveFollowRequest: "Yรชu cแบงu theo dรตi" + followRequestAccepted: "Yรชu cแบงu theo dรตi ฤ‘ฦฐแปฃc chแบฅp nhแบญn" + groupInvited: "Mแปi vร o nhรณm" + app: "Tแปซ app liรชn kแบฟt" + _actions: + followBack: "ฤ‘รฃ theo dรตi lแบกi bแบกn" + reply: "Trแบฃ lแปi" + renote: "ฤฤƒng lแบกi" _deck: + alwaysShowMainColumn: "Luรดn hiแป‡n cแป™t chรญnh" + columnAlign: "Cฤƒn cแป™t" + columnMargin: "Cฤƒn lแป giแปฏa cรกc cแป™t" + columnHeaderHeight: "Chiแปu rแป™ng cแป™t แบฃnh bรฌa" + addColumn: "Thรชm cแป™t" + swapLeft: "Hoรกn ฤ‘แป•i vแป›i cแป™t bรชn trรกi" + swapRight: "Hoรกn ฤ‘แป•i vแป›i cแป™t bรชn phแบฃi" + swapUp: "Hoรกn ฤ‘แป•i vแป›i cแป™t trรชn" + swapDown: "Hoรกn ฤ‘แป•i vแป›i cแป™t dฦฐแป›i" + stackLeft: "Xแบฟp chแป“ng vแป›i cแป™t bรชn trรกi" + popRight: "Xแบฟp chแป“ng vแป›i cแป™t bรชn trรกi" + profile: "Hแป“ sฦก" _columns: + main: "Chรญnh" + widgets: "Tiแป‡n รญch" notifications: "Thรดng bรกo" + tl: "Bแบฃng tin" + antenna: "Trแบกm phรกt sรณng" + list: "Danh sรกch" + mentions: "Lฦฐแปฃt nhแบฏc" + direct: "Nhแบฏn riรชng" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index f644585835..4953f55280 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -8,7 +8,7 @@ notifications: "้€š็Ÿฅ" username: "็”จๆˆทๅ" password: "ๅฏ†็ " forgotPassword: "ๅฟ˜่ฎฐๅฏ†็ " -fetchingAsApObject: "ๅœจ่”้‚ฆๅฎ‡ๅฎ™ๆŸฅ่ฏขไธญ..." +fetchingAsApObject: "ๆญฃๅœจ่”้‚ฆๅฎ‡ๅฎ™ๆŸฅ่ฏขไธญ..." ok: "OK" gotIt: "ๆˆ‘ๆ˜Ž็™ฝไบ†" cancel: "ๅ–ๆถˆ" @@ -69,7 +69,7 @@ exportRequested: "ๅฏผๅ‡บ่ฏทๆฑ‚ๅทฒๆไบค๏ผŒ่ฟ™ๅฏ่ƒฝ้œ€่ฆ่Šฑไธ€ไบ›ๆ—ถ้—ด๏ผŒๅฏผ importRequested: "ๅฏผๅ…ฅ่ฏทๆฑ‚ๅทฒๆไบค๏ผŒ่ฟ™ๅฏ่ƒฝ้œ€่ฆ่Šฑไธ€็‚นๆ—ถ้—ดใ€‚" lists: "ๅˆ—่กจ" noLists: "ๅˆ—่กจไธบ็ฉบ" -note: "ๅ‘ๅธ–" +note: "ๅธ–ๅญ" notes: "ๅธ–ๅญ" following: "ๅ…ณๆณจไธญ" followers: "ๅ…ณๆณจ่€…" @@ -96,7 +96,7 @@ enterEmoji: "่พ“ๅ…ฅ่กจๆƒ…็ฌฆๅท" renote: "่ฝฌๅ‘" unrenote: "ๅ–ๆถˆ่ฝฌๅ‘" renoted: "ๅทฒ่ฝฌๅ‘ใ€‚" -cantRenote: "่ฏฅๅธ–ๅญๆ— ๆณ•่ฝฌๅ‘ใ€‚" +cantRenote: "่ฏฅๅธ–ๆ— ๆณ•่ฝฌๅ‘ใ€‚" cantReRenote: "่ฝฌๅ‘ๆ— ๆณ•่ขซๅ†ๆฌก่ฝฌๅ‘ใ€‚" quote: "ๅผ•็”จ" pinnedNote: "ๅทฒ็ฝฎ้กถ็š„ๅธ–ๅญ" @@ -155,7 +155,7 @@ searchWith: "ๆœ็ดข:{q}" youHaveNoLists: "ๅˆ—่กจไธบ็ฉบ" followConfirm: "ไฝ ็กฎๅฎš่ฆๅ…ณๆณจ{name}ๅ—๏ผŸ" proxyAccount: "ไปฃ็†่ดฆๆˆท" -proxyAccountDescription: "ไปฃ็†ๅธๆˆทๆ˜ฏๅœจๆŸไบ›ๆƒ…ๅ†ตไธ‹ๅ……ๅฝ“็”จๆˆท็š„่ฟœ็จ‹ๅ…ณๆณจ่€…็š„ๅธๆˆทใ€‚ ไพ‹ๅฆ‚๏ผŒๅฝ“ไธ€ไธช็”จๆˆทๅˆ—ๅ‡บไธ€ไธช่ฟœ็จ‹็”จๆˆทๆ—ถ๏ผŒๅฆ‚ๆžœๆฒกๆœ‰ไบบ่ทŸ้š่ฏฅๅˆ—ๅ‡บ็š„็”จๆˆท๏ผŒๅˆ™่ฏฅๆดปๅŠจๅฐ†ไธไผšไผ ้€’ๅˆฐ่ฏฅๅฎžไพ‹๏ผŒๅ› ๆญคๅฐ†ไปฃไน‹ไปฅไปฃ็†ๅธๆˆทใ€‚" +proxyAccountDescription: "ไปฃ็†่ดฆๆˆทๆ˜ฏๅœจๆŸไบ›ๆƒ…ๅ†ตไธ‹ๅ……ๅฝ“็”จๆˆท็š„่ฟœ็จ‹ๅ…ณๆณจ่€…็š„่ดฆๆˆทใ€‚ ไพ‹ๅฆ‚๏ผŒๅฝ“ไธ€ไธช็”จๆˆทๅˆ—ๅ‡บไธ€ไธช่ฟœ็จ‹็”จๆˆทๆ—ถ๏ผŒๅฆ‚ๆžœๆฒกๆœ‰ไบบ่ทŸ้š่ฏฅๅˆ—ๅ‡บ็š„็”จๆˆท๏ผŒๅˆ™่ฏฅๆดปๅŠจๅฐ†ไธไผšไผ ้€’ๅˆฐ่ฏฅๅฎžไพ‹๏ผŒๅ› ๆญคๅฐ†ไปฃไน‹ไปฅไปฃ็†่ดฆๆˆทใ€‚" host: "ไธปๆœบๅ" selectUser: "้€‰ๆ‹ฉ็”จๆˆท" recipient: "ๆ”ถไปถไบบ" @@ -171,7 +171,7 @@ charts: "ๅ›พ่กจ" perHour: "ๆฏๅฐๆ—ถ" perDay: "ๆฏๅคฉ" stopActivityDelivery: "ๅœๆญขๅ‘้€ๆดปๅŠจ" -blockThisInstance: "้˜ปๆญขๆญคๅฎžไพ‹" +blockThisInstance: "้˜ปๆญขๆญคๅฎžไพ‹ๅ‘ๆœฌๅฎžไพ‹ๆŽจๆต" operations: "ๆ“ไฝœ" software: "่ฝฏไปถ" version: "็‰ˆๆœฌ" @@ -250,7 +250,7 @@ messageRead: "ๅทฒ่ฏป" noMoreHistory: "ๆฒกๆœ‰ๆ›ดๅคš็š„ๅŽ†ๅฒ่ฎฐๅฝ•" startMessaging: "ๆทปๅŠ ่Šๅคฉ" nUsersRead: "{n}ไบบๅทฒ่ฏป" -agreeTo: "{0}ไบบๅŒๆ„" +agreeTo: "{0}ๅ‹พ้€‰ๅˆ™่กจ็คบๅทฒ้˜…่ฏปๅนถๅŒๆ„" tos: "ๆœๅŠกๆกๆฌพ" start: "ๅผ€ๅง‹" home: "้ฆ–้กต" @@ -321,7 +321,7 @@ connectService: "่ฟžๆŽฅ" disconnectService: "ๆ–ญๅผ€่ฟžๆŽฅ" enableLocalTimeline: "ๅฏ็”จๆœฌๅœฐๆ—ถ้—ด็บฟๅŠŸ่ƒฝ" enableGlobalTimeline: "ๅฏ็”จๅ…จๅฑ€ๆ—ถ้—ด็บฟ" -disablingTimelinesInfo: "ๅณไฝฟๆ—ถ้—ด็บฟๅŠŸ่ƒฝ่ขซ็ฆ็”จ๏ผŒๅ‡บไบŽไพฟๅˆฉๆ€ง็š„ๅŽŸๅ› ๏ผŒ็ฎก็†ๅ‘˜ๅ’Œๆ•ฐๆฎๅ›พ่กจไนŸๅฏไปฅ็ปง็ปญไฝฟ็”จใ€‚" +disablingTimelinesInfo: "ๅณไฝฟๆ—ถ้—ด็บฟๅŠŸ่ƒฝ่ขซ็ฆ็”จ๏ผŒๅ‡บไบŽๆ–นไพฟ๏ผŒ็ฎก็†ๅ‘˜ๅ’Œๆ•ฐๆฎๅ›พ่กจไนŸๅฏไปฅ็ปง็ปญไฝฟ็”จใ€‚" registration: "ๆณจๅ†Œ" enableRegistration: "ๅ…่ฎธๆ–ฐ็”จๆˆทๆณจๅ†Œ" invite: "้‚€่ฏท" @@ -440,7 +440,7 @@ strongPassword: "ๅฏ†็ ๅผบๅบฆ๏ผšๅผบ" passwordMatched: "ๅฏ†็ ไธ€่‡ด" passwordNotMatched: "ๅฏ†็ ไธไธ€่‡ด" signinWith: "ไปฅ{x}็™ปๅฝ•" -signinFailed: "ๆ— ๆณ•็™ปๅฝ•๏ผŒ่ฏทๆฃ€ๆŸฅๆ‚จ็š„็”จๆˆทๅๅ’Œๅฏ†็ ใ€‚" +signinFailed: "ๆ— ๆณ•็™ปๅฝ•๏ผŒ่ฏทๆฃ€ๆŸฅๆ‚จ็š„็”จๆˆทๅๅ’Œๅฏ†็ ๆ˜ฏๅฆๆญฃ็กฎใ€‚" tapSecurityKey: "่ฝป่งฆ็กฌไปถๅฎ‰ๅ…จๅฏ†้’ฅ" or: "ๆˆ–่€…" language: "่ฏญ่จ€" @@ -459,7 +459,7 @@ category: "็ฑปๅˆซ" tags: "ๆ ‡็ญพ" docSource: "ๆ–‡ไปถๆฅๆบ" createAccount: "ๆณจๅ†Œ่ดฆๆˆท" -existingAccount: "็Žฐๆœ‰็š„ๅธๆˆท" +existingAccount: "็Žฐๆœ‰็š„่ดฆๆˆท" regenerate: "้‡ๆ–ฐ็”Ÿๆˆ" fontSize: "ๅญ—ไฝ“ๅคงๅฐ" noFollowRequests: "ๆฒกๆœ‰ๅ…ณๆณจ็”ณ่ฏท" @@ -533,7 +533,7 @@ removeAllFollowingDescription: "ๅ–ๆถˆ{host}็š„ๆ‰€ๆœ‰ๅ…ณๆณจ่€…ใ€‚ๅฝ“ๅฎžไพ‹ไธๅญ˜ userSuspended: "่ฏฅ็”จๆˆทๅทฒ่ขซๅ†ป็ป“ใ€‚" userSilenced: "่ฏฅ็”จๆˆทๅทฒ่ขซ็ฆ่จ€ใ€‚" yourAccountSuspendedTitle: "่ดฆๆˆทๅทฒ่ขซๅ†ป็ป“" -yourAccountSuspendedDescription: "็”ฑไบŽ่ฟๅไบ†ๆœๅŠกๅ™จ็š„ๆœๅŠกๆกๆฌพๆˆ–ๅ…ถไป–ๅŽŸๅ› ๏ผŒ่ฏฅ่ดฆๆˆทๅทฒ่ขซๅ†ป็ป“ใ€‚ ๆ‚จๅฏไปฅไธŽ็ฎก็†ๅ‘˜่”็ณปไปฅไบ†่งฃๆ›ดๅคšไฟกๆฏใ€‚ ่ฏทไธ่ฆๅˆ›ๅปบไธ€ไธชๆ–ฐ็š„ๅธๆˆทใ€‚" +yourAccountSuspendedDescription: "็”ฑไบŽ่ฟๅไบ†ๆœๅŠกๅ™จ็š„ๆœๅŠกๆกๆฌพๆˆ–ๅ…ถไป–ๅŽŸๅ› ๏ผŒ่ฏฅ่ดฆๆˆทๅทฒ่ขซๅ†ป็ป“ใ€‚ ๆ‚จๅฏไปฅไธŽ็ฎก็†ๅ‘˜่”็ณปไปฅไบ†่งฃๆ›ดๅคšไฟกๆฏใ€‚ ่ฏทไธ่ฆๅˆ›ๅปบไธ€ไธชๆ–ฐ็š„่ดฆๆˆทใ€‚" menu: "่œๅ•" divider: "ๅˆ†ๅ‰ฒ็บฟ" addItem: "ๆทปๅŠ ้กน็›ฎ" @@ -609,7 +609,7 @@ create: "ๅˆ›ๅปบ" notificationSetting: "้€š็Ÿฅ่ฎพ็ฝฎ" notificationSettingDesc: "้€‰ๆ‹ฉ่ฆๆ˜พ็คบ็š„้€š็Ÿฅ็ฑปๅž‹ใ€‚" useGlobalSetting: "ไฝฟ็”จๅ…จๅฑ€่ฎพ็ฝฎ" -useGlobalSettingDesc: "ๅฏ็”จๆ—ถ๏ผŒๅฐ†ไฝฟ็”จๅธๆˆท้€š็Ÿฅ่ฎพ็ฝฎใ€‚ๅ…ณ้—ญๆ—ถ๏ผŒๅˆ™ๅฏไปฅๅ•็‹ฌ่ฎพ็ฝฎใ€‚" +useGlobalSettingDesc: "ๅฏ็”จๆ—ถ๏ผŒๅฐ†ไฝฟ็”จ่ดฆๆˆท้€š็Ÿฅ่ฎพ็ฝฎใ€‚ๅ…ณ้—ญๆ—ถ๏ผŒๅˆ™ๅฏไปฅๅ•็‹ฌ่ฎพ็ฝฎใ€‚" other: "ๅ…ถไป–" regenerateLoginToken: "้‡ๆ–ฐ็”Ÿๆˆ็™ปๅฝ•ไปค็‰Œ" regenerateLoginTokenDescription: "้‡ๆ–ฐ็”Ÿๆˆ็”จไบŽ็™ปๅฝ•็š„ๅ†…้ƒจไปค็‰Œใ€‚้€šๅธธๆ‚จไธ้œ€่ฆ่ฟ™ๆ ทๅšใ€‚้‡ๆ–ฐ็”ŸๆˆๅŽ๏ผŒๆ‚จๅฐ†ๅœจๆ‰€ๆœ‰่ฎพๅค‡ไธŠ็™ปๅ‡บใ€‚" @@ -621,12 +621,12 @@ abuseReports: "ไธพๆŠฅ" reportAbuse: "ไธพๆŠฅ" reportAbuseOf: "ไธพๆŠฅ{name}" fillAbuseReportDescription: "่ฏทๅกซๅ†™ไธพๆŠฅ็š„่ฏฆ็ป†ๅŽŸๅ› ใ€‚ๅฆ‚ๆžœๆœ‰ๅฏนๆ–นๅ‘็š„ๅธ–ๅญ๏ผŒ่ฏทๅŒๆ—ถๅกซๅ†™URLๅœฐๅ€ใ€‚" -abuseReported: "ๅ†…ๅฎนๅทฒๅ‘้€ใ€‚ๆ„Ÿ่ฐขๆ‚จ็š„ๆŠฅๅ‘Šใ€‚" -reporter: "ๆŠฅๅ‘Š่€…" +abuseReported: "ๅ†…ๅฎนๅทฒๅ‘้€ใ€‚ๆ„Ÿ่ฐขๆ‚จๆไบคไฟกๆฏใ€‚" +reporter: "ไธพๆŠฅ่€…" reporteeOrigin: "ไธพๆŠฅๆฅๆบ" reporterOrigin: "ไธพๆŠฅ่€…ๆฅๆบ" -forwardReport: "ๅฐ†ๆŠฅๅ‘Š่ฝฌๅ‘็ป™่ฟœ็จ‹ๅฎžไพ‹" -forwardReportIsAnonymous: "ๅœจ่ฟœ็จ‹ๅฎžไพ‹ไธŠๆ˜พ็คบ็š„ๆŠฅๅ‘Š่€…ๆ˜ฏๅŒฟๅ็š„็ณป็ปŸ่ดฆๅท๏ผŒ่€Œไธๆ˜ฏๆ‚จ็š„่ดฆๅทใ€‚" +forwardReport: "ๅฐ†่ฏฅไธพๆŠฅไฟกๆฏ่ฝฌๅ‘็ป™่ฟœ็จ‹ๅฎžไพ‹" +forwardReportIsAnonymous: "ๅ‹พ้€‰ๅˆ™ๅœจ่ฟœ็จ‹ๅฎžไพ‹ไธŠๆ˜พ็คบ็š„ไธพๆŠฅ่€…ๆ˜ฏๅŒฟๅ็š„็ณป็ปŸ่ดฆๅท๏ผŒ่€Œไธๆ˜ฏๆ‚จ็š„่ดฆๅทใ€‚" send: "ๅ‘้€" abuseMarkAsResolved: "ๅค„็†ๅฎŒๆฏ•" openInNewTab: "ๅœจๆ–ฐๆ ‡็ญพ้กตไธญๆ‰“ๅผ€" @@ -644,9 +644,9 @@ createNew: "ๆ–ฐๅปบ" optional: "ๅฏ้€‰" createNewClip: "ๆ–ฐๅปบไนฆ็ญพ" public: "ๅ…ฌๅผ€" -i18nInfo: "Misskeyๅทฒ็ป่ขซๅฟ—ๆ„ฟ่€…ไปฌ็ฟป่ฏ‘ๅˆฐไบ†ๅ„็ง่ฏญ่จ€ใ€‚ๅฆ‚ๆžœไฝ ไนŸๆœ‰ๅ…ด่ถฃ๏ผŒๅฏไปฅ้€š่ฟ‡{link}ๅธฎๅŠฉ็ฟป่ฏ‘ใ€‚" +i18nInfo: "Misskeyๅทฒ็ป่ขซๅฟ—ๆ„ฟ่€…ไปฌ็ฟป่ฏ‘ๆˆไบ†ๅ„็ง่ฏญ่จ€ใ€‚ๅฆ‚ๆžœไฝ ไนŸๆœ‰ๅ…ด่ถฃ๏ผŒๅฏไปฅ้€š่ฟ‡{link}ๅธฎๅŠฉ็ฟป่ฏ‘ใ€‚" manageAccessTokens: "็ฎก็† Access Tokens" -accountInfo: "ๅธๆˆทไฟกๆฏ" +accountInfo: "่ดฆๆˆทไฟกๆฏ" notesCount: "ๅธ–ๅญๆ•ฐ้‡" repliesCount: "ๅ›žๅคๆ•ฐ้‡" renotesCount: "่ฝฌๅธ–ๆ•ฐ้‡" @@ -662,7 +662,7 @@ yes: "ๆ˜ฏ" no: "ๅฆ" driveFilesCount: "็ฝ‘็›˜็š„ๆ–‡ไปถๆ•ฐ" driveUsage: "็ฝ‘็›˜็š„็ฉบ้—ด็”จ้‡" -noCrawle: "ๆ‹’็ปๆœ็ดขๅผ•ๆ“Ž็š„็ดขๅผ•" +noCrawle: "่ฆๆฑ‚ๆœ็ดขๅผ•ๆ“Žไธ็ดขๅผ•่ฏฅ็ซ™็‚น" noCrawleDescription: "่ฆๆฑ‚ๆœ็ดขๅผ•ๆ“Žไธ่ฆๆ”ถๅฝ•๏ผˆ็ดขๅผ•๏ผ‰ๆ‚จ็š„็”จๆˆท้กต้ข๏ผŒๅธ–ๅญ๏ผŒ้กต้ข็ญ‰ใ€‚" lockedAccountInfo: "ๅณไฝฟ้€š่ฟ‡ไบ†ๅ…ณๆณจ่ฏทๆฑ‚๏ผŒๅช่ฆๆ‚จไธๅฐ†ๅธ–ๅญๅฏ่ง่Œƒๅ›ด่ฎพ็ฝฎๆˆโ€œๅ…ณๆณจ่€…โ€๏ผŒไปปไฝ•ไบบ้ƒฝๅฏไปฅ็œ‹ๅˆฐๆ‚จ็š„ๅธ–ๅญใ€‚" alwaysMarkSensitive: "้ป˜่ฎคๅฐ†ๅช’ไฝ“ๆ–‡ไปถๆ ‡่ฎฐไธบๆ•ๆ„Ÿๅ†…ๅฎน" @@ -1087,7 +1087,6 @@ _sfx: antenna: "ๅคฉ็บฟๆŽฅๆ”ถ" channel: "้ข‘้“้€š็Ÿฅ" _ago: - unknown: "ๆœช็Ÿฅ" future: "ๆœชๆฅ" justNow: "ๆœ€่ฟ‘" secondsAgo: "{n}็ง’ๅ‰" @@ -1131,6 +1130,7 @@ _2fa: registerKey: "ๆณจๅ†Œๅฏ†้’ฅ" step1: "้ฆ–ๅ…ˆ๏ผŒๅœจๆ‚จ็š„่ฎพๅค‡ไธŠๅฎ‰่ฃ…้ชŒ่ฏๅบ”็”จ๏ผŒไพ‹ๅฆ‚{a}ๆˆ–{b}ใ€‚" step2: "็„ถๅŽ๏ผŒๆ‰ซๆๅฑๅน•ไธŠๆ˜พ็คบ็š„ไบŒ็ปด็ ใ€‚" + step2Url: "ๅœจๆกŒ้ขๅบ”็”จ็จ‹ๅบไธญ่พ“ๅ…ฅไปฅไธ‹URL๏ผš" step3: "่พ“ๅ…ฅๆ‚จ็š„ๅบ”็”จๆไพ›็š„ๅŠจๆ€ๅฃไปคไปฅๅฎŒๆˆ่ฎพ็ฝฎใ€‚" step4: "ไปŽ็Žฐๅœจๅผ€ๅง‹๏ผŒไปปไฝ•็™ปๅฝ•ๆ“ไฝœ้ƒฝๅฐ†่ฆๆฑ‚ๆ‚จๆไพ›ๅŠจๆ€ๅฃไปคใ€‚" securityKeyInfo: "ๆ‚จๅฏไปฅ่ฎพ็ฝฎไฝฟ็”จๆ”ฏๆŒFIDO2็š„็กฌไปถๅฎ‰ๅ…จๅฏ†้’ฅใ€่ฎพๅค‡ไธŠ็š„ๆŒ‡็บนๆˆ–PINๆฅไฟๆŠคๆ‚จ็š„็™ปๅฝ•่ฟ‡็จ‹ใ€‚" @@ -1615,6 +1615,7 @@ _notification: yourFollowRequestAccepted: "ๆ‚จ็š„ๅ…ณๆณจ่ฏทๆฑ‚ๅทฒ้€š่ฟ‡" youWereInvitedToGroup: "ๆ‚จๆœ‰ๆ–ฐ็š„็พค็ป„้‚€่ฏท" pollEnded: "้—ฎๅท่ฐƒๆŸฅ็ป“ๆžœๅทฒ็”Ÿๆˆใ€‚" + emptyPushNotificationMessage: "ๆŽจ้€้€š็Ÿฅๅทฒๆ›ดๆ–ฐ" _types: all: "ๅ…จ้ƒจ" follow: "ๅ…ณๆณจไธญ" @@ -1629,6 +1630,10 @@ _notification: followRequestAccepted: "ๅ…ณๆณจ่ฏทๆฑ‚ๅทฒ้€š่ฟ‡" groupInvited: "ๅŠ ๅ…ฅ็พค็ป„้‚€่ฏท" app: "ๅ…ณ่”ๅบ”็”จ็š„้€š็Ÿฅ" + _actions: + followBack: "ๅ›žๅ…ณ" + reply: "ๅ›žๅค" + renote: "่ฝฌๅ‘" _deck: alwaysShowMainColumn: "ๆ€ปๆ˜ฏๆ˜พ็คบไธปๅˆ—" columnAlign: "ๅˆ—ๅฏน้ฝ" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 18c6f17154..f088fdc0e9 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -135,13 +135,14 @@ emojiName: "่กจๆƒ…็ฌฆ่™Ÿๅ็จฑ" emojiUrl: "่กจๆƒ…็ฌฆ่™ŸURL" addEmoji: "ๅŠ ๅ…ฅ่กจๆƒ…็ฌฆ่™Ÿ" settingGuide: "ๆŽจ่–ฆ่จญๅฎš" -cacheRemoteFiles: "็ทฉๅญ˜้ž้ ็จ‹ๆช”ๆกˆ" +cacheRemoteFiles: "ๅฟซๅ–้ ็ซฏๆช”ๆกˆ" cacheRemoteFilesDescription: "็ฆ็”จๆญค่จญๅฎšๆœƒๅœๆญข้ ็ซฏๆช”ๆกˆ็š„็ทฉๅญ˜๏ผŒๅพž่€Œ็ฏ€็œๅ„ฒๅญ˜็ฉบ้–“๏ผŒไฝ†่ณ‡ๆ–™ๆœƒๅ› ็›ดๆŽฅ้€ฃ็ทšๅพž่€Œ็”ข็”Ÿ้กๅค–้€ฃๆŽฅๆ•ธๆ“šใ€‚" flagAsBot: "ๆญคไฝฟ็”จ่€…ๆ˜ฏๆฉŸๅ™จไบบ" flagAsBotDescription: "ๅฆ‚ๆžœๆœฌๅธณๆˆถๆ˜ฏ็”ฑ็จ‹ๅผๆŽงๅˆถ๏ผŒ่ซ‹ๅ•Ÿ็”จๆญค้ธ้ …ใ€‚ๅ•Ÿ็”จๅพŒ๏ผŒๆœƒไฝœ็‚บๆจ™็คบๅนซๅŠฉๅ…ถไป–้–‹็™ผ่€…้˜ฒๆญขๆฉŸๅ™จไบบไน‹้–“็”ข็”Ÿ็„ก้™ไบ’ๅ‹•็š„่กŒ็‚บ๏ผŒไธฆๆœƒ่ชฟๆ•ดMisskeyๅ…ง้ƒจ็ณป็ตฑๅฐ‡ๆœฌๅธณๆˆถ่ญ˜ๅˆฅ็‚บๆฉŸๅ™จไบบ" flagAsCat: "ๆญคไฝฟ็”จ่€…ๆ˜ฏ่ฒ“" flagAsCatDescription: "ๅฆ‚ๆžœๆƒณๅฐ‡ๆœฌๅธณๆˆถๆจ™็คบ็‚บไธ€้šป่ฒ“๏ผŒ่ซ‹้–‹ๅ•Ÿๆญคๆจ™็คบ" flagShowTimelineReplies: "ๅœจๆ™‚้–“่ปธไธŠ้กฏ็คบ่ฒผๆ–‡็š„ๅ›ž่ฆ†" +flagShowTimelineRepliesDescription: "ๅ•Ÿ็”จๆ™‚๏ผŒๆ™‚้–“็ทš้™คไบ†้กฏ็คบ็”จๆˆถ็š„่ฒผๆ–‡ไปฅๅค–๏ผŒ้‚„ๆœƒ้กฏ็คบ็”จๆˆถๅฐๅ…ถไป–่ฒผๆ–‡็š„ๅ›ž่ฆ†ใ€‚" autoAcceptFollowed: "่‡ชๅ‹•่ฟฝ้šจไธญไฝฟ็”จ่€…็š„่ฟฝ้šจ่ซ‹ๆฑ‚" addAccount: "ๆทปๅŠ ๅธณๆˆถ" loginFailed: "็™ปๅ…ฅๅคฑๆ•—" @@ -153,8 +154,8 @@ removeWallpaper: "็งป้™คๆกŒๅธƒ" searchWith: "ๆœๅฐ‹: {q}" youHaveNoLists: "ไฝ ๆฒ’ๆœ‰ไปปไฝ•ๆธ…ๅ–ฎ" followConfirm: "ไฝ ็œŸ็š„่ฆ่ฟฝ้šจ{name}ๅ—Ž๏ผŸ" -proxyAccount: "ไปฃ็†ๅธณ่™Ÿ" -proxyAccountDescription: "ไปฃ็†ๅธณ่™Ÿๆ˜ฏๅœจๆŸไบ›ๆƒ…ๆณไธ‹ๅ……็•ถๅ…ถไป–ไผบๆœๅ™จ็”จๆˆถ็š„ๅธณ่™Ÿใ€‚ไพ‹ๅฆ‚๏ผŒ็•ถไฝฟ็”จ่€…ๅฐ‡ไธ€ๅ€‹ไพ†่‡ชๅ…ถไป–ไผบๆœๅ™จ็š„ๅธณ่™Ÿๆ”พๅœจๅˆ—่กจไธญๆ™‚๏ผŒ็”ฑๆ–ผๆฒ’ๆœ‰ๅ…ถไป–ไฝฟ็”จ่€…้—œๆณจ่ฉฒๅธณ่™Ÿ๏ผŒ่ฉฒๆŒ‡ไปคไธๆœƒๅ‚ณ้€ๅˆฐ่ฉฒไผบๆœๅ™จไธŠ๏ผŒๅ› ๆญคๆœƒ็”ฑไปฃ็†ๅธณๆˆถ้—œๆณจใ€‚" +proxyAccount: "ไปฃ็†ๅธณๆˆถ" +proxyAccountDescription: "ไปฃ็†ๅธณๆˆถๆ˜ฏๅœจๆŸไบ›ๆƒ…ๆณไธ‹ๅ……็•ถๅ…ถไป–ไผบๆœๅ™จ็”จๆˆถ็š„ๅธณๆˆถใ€‚ไพ‹ๅฆ‚๏ผŒ็•ถไฝฟ็”จ่€…ๅฐ‡ไธ€ๅ€‹ไพ†่‡ชๅ…ถไป–ไผบๆœๅ™จ็š„ๅธณๆˆถๆ”พๅœจๅˆ—่กจไธญๆ™‚๏ผŒ็”ฑๆ–ผๆฒ’ๆœ‰ๅ…ถไป–ไฝฟ็”จ่€…้—œๆณจ่ฉฒๅธณๆˆถ๏ผŒ่ฉฒๆŒ‡ไปคไธๆœƒๅ‚ณ้€ๅˆฐ่ฉฒไผบๆœๅ™จไธŠ๏ผŒๅ› ๆญคๆœƒ็”ฑไปฃ็†ๅธณๆˆถ้—œๆณจใ€‚" host: "ไธปๆฉŸ" selectUser: "้ธๅ–ไฝฟ็”จ่€…" recipient: "ๆ”ถไปถไบบ" @@ -197,7 +198,7 @@ noUsers: "ๆฒ’ๆœ‰ไปปไฝ•ไฝฟ็”จ่€…" editProfile: "็ทจ่ผฏๅ€‹ไบบๆช”ๆกˆ" noteDeleteConfirm: "็ขบๅฎšๅˆช้™คๆญค่ฒผๆ–‡ๅ—Ž๏ผŸ" pinLimitExceeded: "ไธ่ƒฝ็ฝฎ้ ‚ๆ›ดๅคš่ฒผๆ–‡ไบ†" -intro: "Misskey ้ƒจ็ฝฒๅฎŒๆˆ๏ผ่ซ‹ๅปบ็ซ‹็ฎก็†ๅ“กๅธณ่™Ÿ๏ผ" +intro: "Misskey ้ƒจ็ฝฒๅฎŒๆˆ๏ผ่ซ‹ๅปบ็ซ‹็ฎก็†ๅ“กๅธณๆˆถใ€‚" done: "ๅฎŒๆˆ" processing: "่™•็†ไธญ" preview: "้ ่ฆฝ" @@ -236,6 +237,8 @@ resetAreYouSure: "็ขบๅฎš่ฆ้‡่จญๅ—Ž๏ผŸ" saved: "ๅทฒๅ„ฒๅญ˜" messaging: "ๅ‚ณ้€่จŠๆฏ" upload: "ไธŠๅ‚ณ" +keepOriginalUploading: "ไฟ็•™ๅŽŸๅœ–" +keepOriginalUploadingDescription: "ไธŠๅ‚ณๅœ–็‰‡ๆ™‚ไฟ็•™ๅŽŸๅง‹ๅœ–็‰‡ใ€‚้—œ้–‰ๆ™‚๏ผŒ็€่ฆฝๅ™จๆœƒๅœจไธŠๅ‚ณๆ™‚็”Ÿๆˆไธ€ๅผต็”จๆ–ผweb็™ผๅธƒ็š„ๅœ–็‰‡ใ€‚" fromDrive: "ๅพž้›ฒ็ซฏ็ฉบ้–“" fromUrl: "ๅพžURL" uploadFromUrl: "ๅพž็ถฒๅ€ไธŠๅ‚ณ" @@ -357,7 +360,7 @@ enableServiceworker: "้–‹ๅ•Ÿ ServiceWorker" antennaUsersDescription: "ๆŒ‡ๅฎš็”จๆ›่กŒ็ฌฆๅˆ†้š”็š„็”จๆˆถๅ" caseSensitive: "ๅ€ๅˆ†ๅคงๅฐๅฏซ" withReplies: "ๅŒ…ๅซๅ›ž่ฆ†" -connectedTo: "ๆ‚จ็š„ๅธณ่™Ÿๅทฒ้€ฃๆŽฅๅˆฐไปฅไธ‹็คพไบคๅธณ่™Ÿ" +connectedTo: "ๆ‚จ็š„ๅธณๆˆถๅทฒ้€ฃๆŽฅๅˆฐไปฅไธ‹็คพไบคๅธณๆˆถ" notesAndReplies: "่ฒผๆ–‡่ˆ‡ๅ›ž่ฆ†" withFiles: "้™„ไปถ" silence: "็ฆ่จ€" @@ -445,6 +448,7 @@ uiLanguage: "ไป‹้ข่ชž่จ€" groupInvited: "ๆ‚จๆœ‰ๆ–ฐ็š„็พค็ต„้‚€่ซ‹" aboutX: "้—œๆ–ผ{x}" useOsNativeEmojis: "ไฝฟ็”จOSๅŽŸ็”Ÿ่กจๆƒ…็ฌฆ่™Ÿ" +disableDrawer: "ไธ้กฏ็คบไธ‹ๆ‹‰ๅผ้ธๅ–ฎ" youHaveNoGroups: "ๆ‰พไธๅˆฐ็พค็ต„" joinOrCreateGroup: "่ซ‹ๅŠ ๅ…ฅ็พๆœ‰็พค็ต„๏ผŒๆˆ–ๅ‰ตๅปบๆ–ฐ็พค็ต„ใ€‚" noHistory: "ๆฒ’ๆœ‰ๆญทๅฒ็ด€้Œ„" @@ -468,7 +472,7 @@ weekOverWeekChanges: "่ˆ‡ไธŠ้€ฑ็›ธๆฏ”" dayOverDayChanges: "่ˆ‡ๅ‰ไธ€ๆ—ฅ็›ธๆฏ”" appearance: "ๅค–่ง€" clientSettings: "็”จๆˆถ็ซฏ่จญๅฎš" -accountSettings: "ๅธณ่™Ÿ่จญๅฎš" +accountSettings: "ๅธณๆˆถ่จญๅฎš" promotion: "ๆŽจๅปฃ" promote: "ๆŽจๅปฃ" numberOfDays: "ๆœ‰ๆ•ˆๅคฉๆ•ธ" @@ -477,6 +481,7 @@ showFeaturedNotesInTimeline: "ๅœจๆ™‚้–“่ปธไธŠ้กฏ็คบ็†ฑ้–€ๆŽจ่–ฆ" objectStorage: "Object Storage (็‰ฉไปถๅ„ฒๅญ˜)" useObjectStorage: "ไฝฟ็”จObject Storage" objectStorageBaseUrl: "Base URL" +objectStorageBaseUrlDesc: "ๅผ•็”จๆ™‚็š„URLใ€‚ๅฆ‚ๆžœๆ‚จไฝฟ็”จ็š„ๆ˜ฏCDNๆˆ–ๅๅ‘ไปฃ็†๏ผŒ่ฏทๆŒ‡ๅฎšๅ…ถURL๏ผŒไพ‹ๅฆ‚S3๏ผšโ€œhttps://.s3.amazonaws.comโ€๏ผŒGCS๏ผšโ€œhttps://storage.googleapis.com/โ€" objectStorageBucket: "ๅ„ฒๅญ˜็ฉบ้–“๏ผˆBucket๏ผ‰" objectStorageBucketDesc: "่ซ‹ๆŒ‡ๅฎšๆ‚จๆญฃๅœจไฝฟ็”จ็š„ๆœๅ‹™็š„ๅญ˜ๅ„ฒๆกถๅ็จฑใ€‚ " objectStoragePrefix: "ๅ‰็ถด" @@ -484,8 +489,11 @@ objectStoragePrefixDesc: "ๅฎƒๅญ˜ๅ„ฒๅœจๆญคๅ‰็ถด็›ฎ้Œ„ไธ‹ใ€‚" objectStorageEndpoint: "็ซฏ้ปž๏ผˆEndpoint๏ผ‰" objectStorageEndpointDesc: "ๅฆ‚่ฆไฝฟ็”จAWS S3๏ผŒ่ซ‹็•™็ฉบใ€‚ๅฆๅ‰‡่ซ‹ไพ็…งไฝ ไฝฟ็”จ็š„ๆœๅ‹™ๅ•†็š„่ชชๆ˜Žๆ›ธ้€ฒ่กŒ่จญๅฎš๏ผŒไปฅ''ๆˆ– ':'็š„ๅฝขๅผ่จญๅฎš็ซฏ้ปž๏ผˆEndpoint๏ผ‰ใ€‚" objectStorageRegion: "ๅœฐๅŸŸ๏ผˆRegion๏ผ‰" +objectStorageRegionDesc: "ๆŒ‡ๅฎšไธ€ๅ€‹ๅˆ†ๅ€๏ผŒไพ‹ๅฆ‚โ€œxx-east-1โ€ใ€‚ ๅฆ‚ๆžœๆ‚จไฝฟ็”จ็š„ๆœๅ‹™ๆฒ’ๆœ‰ๅˆ†ๅ€็š„ๆฆ‚ๅฟต๏ผŒ่ซ‹็•™็ฉบๆˆ–ๅกซๅฏซโ€œus-east-1โ€ใ€‚" objectStorageUseSSL: "ไฝฟ็”จSSL" +objectStorageUseSSLDesc: "ๅฆ‚ๆžœไธไฝฟ็”จhttps้€ฒ่กŒAPI้€ฃๆŽฅ๏ผŒ่ซ‹้—œ้–‰" objectStorageUseProxy: "ไฝฟ็”จ็ถฒ่ทฏไปฃ็†" +objectStorageUseProxyDesc: "ๅฆ‚ๆžœไธไฝฟ็”จไปฃ็†้€ฒ่กŒAPI้€ฃๆŽฅ๏ผŒ่ซ‹้—œ้–‰" objectStorageSetPublicRead: "ไธŠๅ‚ณๆ™‚่จญๅฎš็‚บ\"public-read\"" serverLogs: "ไผบๆœๅ™จๆ—ฅ่ชŒ" deleteAll: "ๅˆช้™คๆ‰€ๆœ‰่จ˜้Œ„" @@ -513,6 +521,7 @@ sort: "ๆŽ’ๅบ" ascendingOrder: "ๆ˜‡ๅ†ช" descendingOrder: "้™ๅ†ช" scratchpad: "ๆšซๅญ˜่จ˜ๆ†ถ้ซ”" +scratchpadDescription: "AiScriptๆŽงๅˆถๅฐ็‚บAiScriptๆไพ›ไบ†ๅฏฆ้ฉ—็’ฐๅขƒใ€‚ๆ‚จๅฏไปฅๅœจๆญค็ทจๅฏซใ€ๅŸท่กŒๅ’Œ็ขบ่ชไปฃ็ขผ่ˆ‡Misskeyไบ’ๅ‹•็š„็ป“ๆžœใ€‚" output: "่ผธๅ‡บ" script: "่…ณๆœฌ" disablePagesScript: "ๅœ็”จ้ ้ข็š„AiScript่…ณๆœฌ" @@ -523,6 +532,9 @@ removeAllFollowing: "่งฃ้™คๆ‰€ๆœ‰่ฟฝ่นค" removeAllFollowingDescription: "่งฃ้™ค{host}ๆ‰€ๆœ‰็š„่ฟฝ่นคใ€‚ๅœจๅฏฆไพ‹ไธๅ†ๅญ˜ๅœจๆ™‚ๅŸท่กŒใ€‚" userSuspended: "่ฉฒไฝฟ็”จ่€…ๅทฒ่ขซๅœ็”จ" userSilenced: "่ฉฒ็”จๆˆถๅทฒ่ขซ็ฆ่จ€ใ€‚" +yourAccountSuspendedTitle: "ๅธณๆˆถๅทฒ่ขซๅ‡็ต" +yourAccountSuspendedDescription: "็”ฑๆ–ผ้•ๅไบ†ไผบๆœๅ™จ็š„ๆœๅ‹™ๆขๆฌพๆˆ–ๅ…ถไป–ๅŽŸๅ› ๏ผŒ่ฉฒๅธณๆˆถๅทฒ่ขซๅ‡็ตใ€‚ ๆ‚จๅฏไปฅ่ˆ‡็ฎก็†ๅ“ก้€ฃ็นซไปฅไบ†่งฃๆ›ดๅคš่จŠๆฏใ€‚ ่ซ‹ไธ่ฆๅ‰ตๅปบไธ€ๅ€‹ๆ–ฐ็š„ๅธณๆˆถใ€‚" +menu: "้ธๅ–ฎ" divider: "ๅˆ†ๅ‰ฒ็ทš" addItem: "ๆ–ฐๅขž้ …็›ฎ" relays: "ไธญ็นผ" @@ -546,7 +558,7 @@ enterFileDescription: "่ผธๅ…ฅๆจ™้กŒ " author: "ไฝœ่€…" leaveConfirm: "ๆœ‰ๆœชไฟๅญ˜็š„ๆ›ดๆ”นใ€‚่ฆๆ”พๆฃ„ๅ—Ž๏ผŸ" manage: "็ฎก็†" -plugins: "ๆ’ไปถ" +plugins: "ๅค–ๆŽ›" deck: "ๅคšๆฌ„ๆจกๅผ" undeck: "ๅ–ๆถˆๅคšๆฌ„ๆจกๅผ" useBlurEffectForModal: "ๅœจๆจกๆ…‹ๆก†ไฝฟ็”จๆจก็ณŠๆ•ˆๆžœ" @@ -556,10 +568,12 @@ height: "้ซ˜ๅบฆ" large: "ๅคง" medium: "ไธญ" small: "ๅฐ" +generateAccessToken: "็™ผ่กŒๅญ˜ๅ–ๆฌŠๆ–" permission: "ๆฌŠ้™" enableAll: "ๅ•Ÿ็”จๅ…จ้ƒจ" disableAll: "ๅœ็”จๅ…จ้ƒจ" -tokenRequested: "ๅ…่จฑๅญ˜ๅ–ๅธณ่™Ÿ" +tokenRequested: "ๅ…่จฑๅญ˜ๅ–ๅธณๆˆถ" +pluginTokenRequestedDescription: "ๆญคๅค–ๆŽ›ๅฐ‡ๆ“ๆœ‰ๅœจๆญค่จญๅฎš็š„ๆฌŠ้™ใ€‚" notificationType: "้€š็Ÿฅๅฝขๅผ" edit: "็ทจ่ผฏ" useStarForReactionFallback: "ไปฅโ˜…ไปฃๆ›ฟๆœช็Ÿฅ็š„่กจๆƒ…็ฌฆ่™Ÿ" @@ -574,8 +588,13 @@ smtpPort: "ๅŸ " smtpUser: "ไฝฟ็”จ่€…ๅ็จฑ" smtpPass: "ๅฏ†็ขผ" emptyToDisableSmtpAuth: "็•™็ฉบไฝฟ็”จ่€…ๅ็จฑๅ’Œๅฏ†็ขผไปฅ้—œ้–‰SMTP้ฉ—่ญ‰ใ€‚" +smtpSecure: "ๅœจ SMTP ้€ฃๆŽฅไธญไฝฟ็”จ้šฑๅผ SSL/TLS" +smtpSecureInfo: "ไฝฟ็”จSTARTTLSๆ™‚้—œ้–‰ใ€‚" testEmail: "ๆธฌ่ฉฆ้ƒตไปถ็™ผ้€" -wordMute: "้œ้Ÿณๆ–‡ๅญ—" +wordMute: "่ขซ้œ้Ÿณ็š„ๆ–‡ๅญ—" +regexpError: "ๆญฃ่ฆ่กจ้”ๅผ้Œฏ่ชค" +regexpErrorDescription: "{tab} ้œ้Ÿณๆ–‡ๅญ—็š„็ฌฌ {line} ่กŒ็š„ๆญฃ่ฆ่กจ้”ๅผๆœ‰้Œฏ่ชค๏ผš" +instanceMute: "ๅฏฆไพ‹็š„้œ้Ÿณ" userSaysSomething: "{name}่ชชไบ†ไป€้บผ" makeActive: "ๅ•Ÿ็”จ" display: "ๆชข่ฆ–" @@ -606,6 +625,8 @@ abuseReported: "ๅ›žๅ ฑๅทฒ้€ๅ‡บใ€‚ๆ„Ÿ่ฌๆ‚จ็š„ๅ ฑๅ‘Šใ€‚" reporter: "ๆชข่ˆ‰่€…" reporteeOrigin: "ๆชข่ˆ‰ไพ†ๆบ" reporterOrigin: "ๆชข่ˆ‰่€…ไพ†ๆบ" +forwardReport: "ๅฐ‡ๅ ฑๅ‘Š่ฝ‰้€็ตฆ้ ็ซฏๅฏฆไพ‹" +forwardReportIsAnonymous: "ๅœจ้ ็ซฏๅฏฆไพ‹ไธŠ็œ‹ไธๅˆฐๆ‚จ็š„่ณ‡่จŠ๏ผŒ้กฏ็คบ็š„ๅ ฑๅ‘Š่€…ๆ˜ฏๅŒฟๅ็š„็ณป็ปŸๅธณๆˆถใ€‚" send: "็™ผ้€" abuseMarkAsResolved: "่™•็†ๅฎŒ็•ข" openInNewTab: "ๅœจๆ–ฐๅˆ†้ ไธญ้–‹ๅ•Ÿ" @@ -667,6 +688,7 @@ center: "็ฝฎไธญ" wide: "ๅฏฌ" narrow: "็ช„" reloadToApplySetting: "่จญๅฎšๅฐ‡ๆœƒๅœจ้ ้ข้‡ๆ–ฐ่ผ‰ๅ…ฅไน‹ๅพŒ็”Ÿๆ•ˆใ€‚่ฆ็พๅœจๅฐฑ้‡่ผ‰้ ้ขๅ—Ž๏ผŸ" +needReloadToApply: "ๅฟ…้ ˆ้‡ๆ–ฐ่ผ‰ๅ…ฅๆ‰ๆœƒ็”Ÿๆ•ˆใ€‚" showTitlebar: "้กฏ็คบๆจ™้กŒๅˆ—" clearCache: "ๆธ…้™คๅฟซๅ–่ณ‡ๆ–™" onlineUsersCount: "{n}ไบบๆญฃๅœจ็ทšไธŠ" @@ -727,6 +749,7 @@ notRecommended: "ไธๆŽจ่–ฆ" botProtection: "Bot้˜ฒ่ญท" instanceBlocking: "ๅทฒๅฐ้Ž–็š„ๅฏฆไพ‹" selectAccount: "้ธๆ“‡ๅธณๆˆถ" +switchAccount: "ๅˆ‡ๆ›ๅธณๆˆถ" enabled: "ๅทฒๅ•Ÿ็”จ" disabled: "ๅทฒๅœ็”จ" quickAction: "ๅฟซๆทๆ“ไฝœ" @@ -753,32 +776,92 @@ emailNotConfiguredWarning: "ๆฒ’ๆœ‰่จญๅฎš้›ปๅญ้ƒตไปถๅœฐๅ€" ratio: "%" previewNoteText: "้ ่ฆฝๆ–‡ๆœฌ" customCss: "่‡ชๅฎš็พฉ CSS" +customCssWarn: "้€™ๅ€‹่จญๅฎšๅฟ…้ ˆ็”ฑๅ…ทๅ‚™็›ธ้—œ็Ÿฅ่ญ˜็š„ไบบๅ“กๆ“ไฝœ๏ผŒไธ็•ถ็š„่จญๅฎšๅฏ่ƒฝๅฏผ่‡ดๅฎขๆˆถ็ซฏ็„กๆณ•ๆญฃๅธธไฝฟ็”จใ€‚" global: "ๅ…ฌ้–‹" +squareAvatars: "้ ญๅƒไปฅๆ–นๅฝข้กฏ็คบ" sent: "็™ผ้€" received: "ๆ”ถๅ–" searchResult: "ๆœๅฐ‹็ตๆžœ" hashtags: "#tag" troubleshooting: "ๆ•…้šœๆŽ’้™ค" useBlurEffect: "ๅœจ UI ไธŠไฝฟ็”จๆจก็ณŠๆ•ˆๆžœ" +learnMore: "ๆ›ดๅคš่ณ‡่จŠ" misskeyUpdated: "Misskey ๆ›ดๆ–ฐๅฎŒๆˆ๏ผ" +whatIsNew: "้กฏ็คบๆ›ดๆ–ฐ่ณ‡่จŠ" translate: "็ฟป่ญฏ" translatedFrom: "ๅพž {x} ็ฟป่ญฏ" accountDeletionInProgress: "ๆญฃๅœจๅˆช้™คๅธณๆˆถ" +usernameInfo: "ๅœจไผบๆœๅ™จไธŠๆ‚จ็š„ๅธณๆˆถๆ˜ฏๅ”ฏไธ€็š„่ญ˜ๅˆฅๅ็จฑใ€‚ๆ‚จๅฏไปฅไฝฟ็”จๅญ—ๆฏ (a ~ z, A ~ Z)ใ€ๆ•ธๅญ— (0 ~ 9) ๅ’Œไธ‹ๅบ•็ทš (_)ใ€‚ไน‹ๅพŒๅธณๆˆถๅๆ˜ฏไธ่ƒฝๆ›ดๆ”น็š„ใ€‚" +aiChanMode: "ๅฐ่—ๆจกๅผ" +keepCw: "ไฟๆŒCW" pubSub: "Pub/Sub ๅธณๆˆถ" +lastCommunication: "ๆœ€่ฟ‘็š„้€šไฟก" resolved: "ๅทฒ่งฃๆฑบ" unresolved: "ๆœช่งฃๆฑบ" breakFollow: "็งป้™ค่ฟฝ่นค่€…" +itsOn: "ๅทฒ้–‹ๅ•Ÿ" +itsOff: "ๅทฒ้—œ้–‰" +emailRequiredForSignup: "่จปๅ†Šๅธณๆˆถ้œ€่ฆ้›ปๅญ้ƒตไปถๅœฐๅ€" +unread: "ๆœช่ฎ€" +filter: "็ฏฉ้ธ" +controlPanel: "ๆŽงๅˆถๅฐ" +manageAccounts: "็ฎก็†ๅธณๆˆถ" +makeReactionsPublic: "ๅฐ‡ๅ›žๆ‡‰่จญ็‚บๅ…ฌ้–‹" +makeReactionsPublicDescription: "ๅฐ‡ๆ‚จๅš้Ž็š„ๅ›žๆ‡‰่จญ็‚บๅ…ฌ้–‹ๅฏ่ฆ‹ใ€‚" +classic: "็ถ“ๅ…ธ" +muteThread: "ๅฐ‡่ฒผๆ–‡ไธฒ่จญ็‚บ้œ้Ÿณ" +unmuteThread: "ๅฐ‡่ฒผๆ–‡ไธฒ็š„้œ้Ÿณ่งฃ้™ค" +ffVisibility: "้€ฃๆŽฅ็š„ๅ…ฌ้–‹็ฏ„ๅœ" +ffVisibilityDescription: "ๆ‚จๅฏไปฅ่จญๅฎšๆ‚จ็š„้—œๆณจ/้—œๆณจ่€…่ณ‡่จŠ็š„ๅ…ฌ้–‹็ฏ„ๅœ" +continueThread: "ๆŸฅ็œ‹ๆ›ดๅคš่ฒผๆ–‡" +deleteAccountConfirm: "ๅฐ‡่ฆๅˆช้™คๅธณๆˆถใ€‚ๆ˜ฏๅฆ็ขบๅฎš๏ผŸ" +incorrectPassword: "ๅฏ†็ขผ้Œฏ่ชคใ€‚" +voteConfirm: "็ขบๅฎšๆŠ•็ตฆใ€Œ{choice}ใ€๏ผŸ" hide: "้šฑ่—" +leaveGroup: "้›ข้–‹็พค็ต„" leaveGroupConfirm: "็ขบๅฎš้›ข้–‹ใ€Œ{name}ใ€๏ผŸ" +useDrawerReactionPickerForMobile: "ๅœจ็งปๅ‹•่จญๅ‚™ไธŠไฝฟ็”จๆŠฝๅฑœ้กฏ็คบ" +welcomeBackWithName: "ๆญก่ฟŽๅ›žไพ†๏ผŒ{name}" +clickToFinishEmailVerification: "้ปžๆ“Š [{ok}] ๅฎŒๆˆ้›ปๅญ้ƒตไปถๅœฐๅ€่ช่ญ‰ใ€‚" +overridedDeviceKind: "่ฃ็ฝฎ้กžๅž‹" +smartphone: "ๆ™บๆ…งๅž‹ๆ‰‹ๆฉŸ" +tablet: "ๅนณๆฟ" auto: "่‡ชๅ‹•" +themeColor: "ไธป้กŒ้ก่‰ฒ" +size: "ๅคงๅฐ" +numberOfColumn: "ๅˆ—ๆ•ธ" searchByGoogle: "ๆœๅฐ‹" +instanceDefaultLightTheme: "ๅฏฆไพ‹้ ่จญ็š„ๆทบ่‰ฒไธป้กŒ" +instanceDefaultDarkTheme: "ๅฏฆไพ‹้ ่จญ็š„ๆทฑ่‰ฒไธป้กŒ" +instanceDefaultThemeDescription: "่ผธๅ…ฅ็‰ฉไปถๅฝขๅผ็š„ไธป้ข˜ไปฃ็ขผ" +mutePeriod: "้œ้Ÿณ็š„ๆœŸ้™" indefinitely: "็„กๆœŸ้™" +tenMinutes: "10ๅˆ†้˜" +oneHour: "1ๅฐๆ™‚" +oneDay: "1ๅคฉ" +oneWeek: "1้€ฑ" +reflectMayTakeTime: "ๅฏ่ƒฝ้œ€่ฆไธ€ไบ›ๆ™‚้–“ๆ‰ๆœƒๅ‡บ็พๆ•ˆๆžœใ€‚" +failedToFetchAccountInformation: "ๅ–ๅพ—ๅธณๆˆถ่ณ‡่จŠๅคฑๆ•—" +_emailUnavailable: + used: "ๅทฒ็ถ“ๅœจไฝฟ็”จไธญ" + format: "ๆ ผๅผ็„กๆ•ˆ" + disposable: "ไธๆ˜ฏๆฐธไน…ๅฏ็”จ็š„ๅœฐๅ€" + mx: "้ƒตไปถไผบๆœๅ™จไธๆญฃ็ขบ" + smtp: "้ƒตไปถไผบๆœๅ™จๆฒ’ๆœ‰ๆ‡‰็ญ”" _ffVisibility: public: "็™ผไฝˆ" + followers: "ๅชๆœ‰้—œๆณจไฝ ็š„็”จๆˆถ่ƒฝ็œ‹ๅˆฐ" private: "็งๅฏ†" _signup: almostThere: "ๅณๅฐ‡ๅฎŒๆˆ" + emailAddressInfo: "่ซ‹่ผธๅ…ฅๆ‚จๆ‰€ไฝฟ็”จ็š„้›ปๅญ้ƒตไปถๅœฐๅ€ใ€‚้›ปๅญ้ƒตไปถๅœฐๅ€ไธๆœƒ่ขซๅ…ฌ้–‹ใ€‚" + emailSent: "ๅทฒๅฐ‡็ขบ่ช้ƒตไปถ็™ผ้€่‡ณๆ‚จ่ผธๅ…ฅ็š„้›ปๅญ้ƒตไปถๅœฐๅ€ ({email})ใ€‚่ซ‹้–‹ๅ•Ÿ้›ปๅญ้ƒตไปถไธญ็š„้€ฃ็ตไปฅๅฎŒๆˆๅธณๆˆถๅ‰ตๅปบใ€‚" _accountDelete: + accountDelete: "ๅˆช้™คๅธณๆˆถ" + mayTakeTime: "ๅˆช้™คๅธณๆˆถ็š„่™•็†่ฒ ่ท่ผƒๅคง๏ผŒๅฆ‚ๆžœๅธณๆˆถ็”ข็”Ÿ็š„ๅ…งๅฎนๆ•ธ้‡ไธŠ่ˆน็š„ๆช”ๆกˆๆ•ธ้‡่ผƒๅคš็š„่ฉฑ๏ผŒๅฐฑ้œ€่ฆ่Šฑ่ดนไธ€ๆฎตๆ™‚้–“ๆ‰่ƒฝๅฎŒๆˆใ€‚" + sendEmail: "ๅธณๆˆถๅˆ ้™คๅฎŒๆˆๅพŒ๏ผŒๅฐ‡ๅ‘่จปๅ†Šๅœฐ้›ปๅญ้ƒตไปถๅœฐๅ€็™ผ้€้€š็Ÿฅใ€‚" + requestAccountDelete: "ๅˆช้™คๅธณๆˆถ่ซ‹ๆฑ‚" + started: "ๅทฒ้–‹ๅง‹ๅˆช้™คไฝœๆฅญใ€‚" inProgress: "ๆญฃๅœจๅˆช้™ค" _ad: back: "่ฟ”ๅ›ž" @@ -800,7 +883,7 @@ _email: _plugin: install: "ๅฎ‰่ฃๅค–ๆŽ›็ต„ไปถ" installWarn: "่ซ‹ไธ่ฆๅฎ‰่ฃไพ†ๆบไธๆ˜Ž็š„ๅค–ๆŽ›็ต„ไปถใ€‚" - manage: "็ฎก็†ๆ’ไปถ" + manage: "็ฎก็†ๅค–ๆŽ›" _registry: scope: "็ฏ„ๅœ" key: "ๆฉŸ็ขผ" @@ -833,14 +916,21 @@ _mfm: link: "้ˆๆŽฅ" linkDescription: "ๆ‚จๅฏไปฅๅฐ‡็‰นๅฎš็ฏ„ๅœ็š„ๆ–‡็ซ ่ˆ‡ URL ็›ธ้—œ่ฏใ€‚ " bold: "็ฒ—้ซ”" + boldDescription: "ๅฏไปฅๅฐ‡ๆ–‡ๅญ—้กฏ็คบไธบ็ฒ—้ซ”ๆฅๅผท่ชฟใ€‚" small: "็ธฎๅฐ" + smallDescription: "ๅฏไปฅไฝฟๅ…งๅฎนๆ–‡ๅญ—่ฎŠๅฐใ€่ฎŠๆทกใ€‚" center: "็ฝฎไธญ" + centerDescription: "ๅฏไปฅๅฐ‡ๅ…งๅฎน็ฝฎไธญ้กฏ็คบใ€‚" inlineCode: "็จ‹ๅผ็ขผ(ๅ†…ๅตŒ)" + inlineCodeDescription: "ๅœจ่กŒๅ…ง็”จ้ซ˜ไบฎๅบฆ้กฏ็คบ๏ผŒไพ‹ๅฆ‚็จ‹ๅผ็ขผ่ชžๆณ•ใ€‚" blockCode: "็จ‹ๅผ็ขผ(ๅ€ๅกŠ)" + blockCodeDescription: "ๅœจๅ€ๅกŠไธญ็”จ้ซ˜ไบฎๅบฆ้กฏ็คบ๏ผŒไพ‹ๅฆ‚่ค‡ๆ•ธ่กŒ็š„็จ‹ๅผ็ขผ่ชžๆณ•ใ€‚" inlineMath: "ๆ•ธๅญธๅ…ฌๅผ(ๅ…งๅตŒ)" inlineMathDescription: "้กฏ็คบๅ…งๅตŒ็š„KaTexๆ•ธๅญธๅ…ฌๅผใ€‚" blockMath: "ๆ•ธๅญธๅ…ฌๅผ(ๆ–นๅกŠ)" + blockMathDescription: "ไปฅๅ€ๅกŠ้กฏ็คบ่ค‡ๆ•ธ่กŒ็š„KaTexๆ•ธๅญธๅผใ€‚" quote: "ๅผ•็”จ" + quoteDescription: "ๅฏไปฅ็”จไพ†่กจ็คบๅผ•็”จ็š„ๅ†…ๅฎนใ€‚" emoji: "่‡ช่จ‚่กจๆƒ…็ฌฆ่™Ÿ" emojiDescription: "ๆ‚จๅฏไปฅ้€š้Žๅฐ‡่‡ชๅฎš็พฉ่กจๆƒ…็ฌฆ่™Ÿๅ็จฑๆ‹ฌๅœจๅ†’่™Ÿไธญไพ†้กฏ็คบ่‡ชๅฎš็พฉ่กจๆƒ…็ฌฆ่™Ÿใ€‚ " search: "ๆœๅฐ‹" @@ -849,22 +939,34 @@ _mfm: flipDescription: "ๅฐ‡ๅ…งๅฎนไธŠไธ‹ๆˆ–ๅทฆๅณ็ฟป่ฝ‰ใ€‚" jelly: "ๅ‹•็•ซ(ๆžœๅ‡)" jellyDescription: "้กฏ็คบๆžœๅ‡ไธ€ๆจฃ็š„ๅ‹•็•ซๆ•ˆๆžœใ€‚" + tada: "ๅ‹•็•ซ๏ผˆ้˜๏ฝž๏ผ‰" + tadaDescription: "้กฏ็คบใ€Œ้˜๏ฝž๏ผใ€้€™็จฎๆ„Ÿ่ฆบ็š„ๅ‹•็•ซๆ•ˆๆžœใ€‚" jump: "ๅ‹•็•ซ(่ทณๅ‹•)" + jumpDescription: "้กฏ็คบ่ทณๅ‹•็š„ๅ‹•็•ซๆ•ˆๆžœใ€‚" bounce: "ๅ‹•็•ซ(ๅๅฝˆ)" + bounceDescription: "้กฏ็คบๆœ‰ๅฝˆๆ€ง็š„ๅ‹•็•ซๆ•ˆๆžœใ€‚" shake: "ๅ‹•็•ซ(ๆ–ๆ™ƒ)" + shakeDescription: "้กฏ็คบ้กซๆŠ–็š„ๅ‹•็•ซๆ•ˆๆžœใ€‚" twitch: "ๅ‹•็•ซ(้กซๆŠ–)" twitchDescription: "้กฏ็คบๅผท็ƒˆ้กซๆŠ–็š„ๅ‹•็•ซๆ•ˆๆžœใ€‚" spin: "ๅ‹•็•ซ(ๆ—‹่ฝ‰)" spinDescription: "้กฏ็คบๆ—‹่ฝ‰็š„ๅ‹•็•ซๆ•ˆๆžœใ€‚" x2: "ๅคง" + x2Description: "ๆ”พๅคง้กฏ็คบๅ…งๅฎนใ€‚" x3: "่ผƒๅคง" x3Description: "ๆ”พๅคง้กฏ็คบๅ…งๅฎนใ€‚" x4: "ๆœ€ๅคง" x4Description: "ๅฐ‡้กฏ็คบๅ…งๅฎนๆ”พ่‡ณๆœ€ๅคงใ€‚" blur: "ๆจก็ณŠ" + blurDescription: "็”ข็”Ÿๆจก็ณŠๆ•ˆๆžœใ€‚ๅฐ†ๆธธๆจ™ๆ”พๅœจไธŠ้ขๅณๅฏๅฐ‡ๅ†…ๅฎน้กฏ็คบๅ‡บไพ†ใ€‚" font: "ๅญ—ๅž‹" fontDescription: "ๆ‚จๅฏไปฅ่จญๅฎš้กฏ็คบๅ…งๅฎน็š„ๅญ—ๅž‹" + rainbow: "ๅฝฉ่™น" + rainbowDescription: "็”จๅฝฉ่™น่‰ฒไพ†้กฏ็คบๅ…งๅฎนใ€‚" + sparkle: "้–ƒ้–ƒ็™ผๅ…‰" + sparkleDescription: "ๆทปๅŠ ้–ƒ้–ƒ็™ผๅ…‰็š„็ฒ’ๅญๆ•ˆๆžœใ€‚" rotate: "ๆ—‹่ฝ‰" + rotateDescription: "ไปฅๆŒ‡ๅฎš็š„่ง’ๅบฆๆ—‹่ฝ‰ใ€‚" _instanceTicker: none: "้šฑ่—" remote: "ๅ‘้ ็ซฏไฝฟ็”จ่€…้กฏ็คบ" @@ -884,11 +986,24 @@ _channel: usersCount: "ๆœ‰{n}ไบบๅƒ่ˆ‡" notesCount: "ๆœ‰{n}ๅ€‹่ฒผๆ–‡" _menuDisplay: + sideFull: "ๅดๅ‘" + sideIcon: "ๅดๅ‘๏ผˆๅœ–็คบ๏ผ‰" + top: "้ ‚้ƒจ" hide: "้šฑ่—" _wordMute: muteWords: "ๅŠ ๅ…ฅ้œ้Ÿณๆ–‡ๅญ—" + muteWordsDescription: "็”จ็ฉบๆ ผๅˆ†้š”ๆŒ‡ๅฎšAND๏ผŒ็”จๆ›่กŒๅˆ†้š”ๆŒ‡ๅฎšORใ€‚" + muteWordsDescription2: "ๅฐ‡้—œ้ตๅญ—็”จๆ–œ็ทšๆ‹ฌ่ตทไพ†่กจ็คบๆญฃ่ฆ่กจ้”ๅผใ€‚" softDescription: "้šฑ่—ๆ™‚้–“่ปธไธญๆŒ‡ๅฎšๆขไปถ็š„่ฒผๆ–‡ใ€‚" + hardDescription: "ๅ…ทๆœ‰ๆŒ‡ๅฎšๆขไปถ็š„่ฒผๆ–‡ๅฐ‡ไธๆทปๅŠ ๅˆฐๆ™‚้–“่ปธใ€‚ ๅณไฝฟๆ‚จๆ›ดๆ”นๆขไปถ๏ผŒๆœช่ขซๆทปๅŠ ็š„่ฒผๆ–‡ไนŸๆœƒ่ขซๆŽ’้™คๅœจๅค–ใ€‚" + soft: "่ปŸๆ€ง้œ้Ÿณ" + hard: "็กฌๆ€ง้œ้Ÿณ" mutedNotes: "ๅทฒ้œ้Ÿณ็š„่ฒผๆ–‡" +_instanceMute: + instanceMuteDescription: "ๅŒ…ๆ‹ฌๅฐ่ขซ้œ้Ÿณๅฏฆไพ‹ไธŠ็š„็”จๆˆถ็š„ๅ›ž่ฆ†๏ผŒ่ขซ่จญๅฎš็š„ๅฏฆไพ‹ไธŠๆ‰€ๆœ‰่ฒผๆ–‡ๅŠ่ฝ‰็™ผ้ƒฝๆœƒ่ขซ้œ้Ÿณใ€‚" + instanceMuteDescription2: "่จญๅฎšๆ™‚ไปฅๆ›่กŒ้€ฒ่กŒๅˆ†้š”" + title: "่ขซ่จญๅฎš็š„ๅฏฆไพ‹๏ผŒ่ฒผๆ–‡ๅฐ‡่ขซ้šฑ่—ใ€‚" + heading: "ๅฐ‡ๅฏฆไพ‹้œ้Ÿณ" _theme: explore: "ๅ–ๅพ—ไฝˆๆ™ฏไธป้กŒ" install: "ๅฎ‰่ฃไฝˆๆ™ฏไธป้กŒ" @@ -902,10 +1017,12 @@ _theme: invalid: "ไธป้กŒๆ ผๅผ้Œฏ่ชค" make: "่ฃฝไฝœไธป้กŒ" base: "ๅŸบๆ–ผ" + addConstant: "ๆทปๅŠ ๅธธๆ•ธ" constant: "ๅธธๆ•ธ" defaultValue: "้ ่จญๅ€ผ" color: "้ก่‰ฒ" refProp: "ๆŸฅ็œ‹ๅฑฌๆ€ง " + refConst: "ๆŸฅ็œ‹ๅธธๆ•ธ" key: "ๆŒ‰้ต" func: "ๅ‡ฝๆ•ฐ" funcKind: "ๅŠŸ่ƒฝ้กžๅž‹" @@ -914,6 +1031,9 @@ _theme: alpha: "้€ๆ˜Žๅบฆ" darken: "ๆš—ๅบฆ" lighten: "ไบฎๅบฆ" + inputConstantName: "่ซ‹่ผธๅ…ฅๅธธๆ•ธ็š„ๅ็จฑ" + importInfo: "ๆ‚จๅฏไปฅๅœจๆญค่ฒผไธŠไธป้กŒไปฃ็ขผ๏ผŒๅฐ‡ๅ…ถๅŒฏๅ…ฅ็ทจ่ผฏๅ™จไธญ" + deleteConstantConfirm: "็ขบๅฎš่ฆๅˆ ้™คๅธธๆ•ธ{const}ๅ—Ž๏ผŸ" keys: accent: "้‡้ปž่‰ฒๅฝฉ" bg: "่ƒŒๆ™ฏ" @@ -933,6 +1053,7 @@ _theme: mention: "ๆๅˆฐ" mentionMe: "ๆๅˆฐไบ†ๆˆ‘" renote: "่ฝ‰็™ผ่ฒผๆ–‡" + modalBg: "ๅฐ่ฉฑๆก†่ƒŒๆ™ฏ" divider: "ๅˆ†ๅ‰ฒ็ทš" scrollbarHandle: "ๆฒๅ‹•ๆข" scrollbarHandleHover: "ๆฒๅ‹•ๆข (ๆผ‚ๆตฎ)" @@ -966,7 +1087,6 @@ _sfx: antenna: "ๅคฉ็ทšๆŽฅๆ”ถ" channel: "้ ป้“้€š็Ÿฅ" _ago: - unknown: "ๆœช็Ÿฅ" future: "ๆœชไพ†" justNow: "ๅ‰›ๅ‰›" secondsAgo: "{n}็ง’ๅ‰" @@ -1010,9 +1130,13 @@ _2fa: registerKey: "่จปๅ†Š้ต" step1: "้ฆ–ๅ…ˆ๏ผŒๅœจๆ‚จ็š„่จญๅ‚™ไธŠๅฎ‰่ฃไบŒๆญฅ้ฉ—่ญ‰็จ‹ๅผ๏ผŒไพ‹ๅฆ‚{a}ๆˆ–{b}ใ€‚" step2: "็„ถๅพŒ๏ผŒๆŽƒๆ่žขๅน•ไธŠ็š„QR codeใ€‚" + step2Url: "ๅœจๆกŒ้ข็‰ˆๆ‡‰็”จไธญ๏ผŒ่ซ‹่ผธๅ…ฅไปฅไธ‹็š„URL๏ผš" + step3: "่ผธๅ…ฅๆ‚จ็š„Appๆไพ›็š„ๆฌŠๆ–ไปฅๅฎŒๆˆ่จญๅฎšใ€‚" + step4: "ๅพž็พๅœจ้–‹ๅง‹๏ผŒไปปไฝ•็™ปๅ…ฅๆ“ไฝœ้ƒฝๅฐ‡่ฆๆฑ‚ๆ‚จๆไพ›ๆฌŠๆ–ใ€‚" + securityKeyInfo: "ๆ‚จๅฏไปฅ่จญๅฎšไฝฟ็”จๆ”ฏๆดFIDO2็š„็กฌ้ซ”ๅฎ‰ๅ…จ้Ž–ใ€็ต‚็ซฏ่จญๅ‚™็š„ๆŒ‡็บน่ช่ญ‰ๆˆ–่€…PIN็ขผไพ†็™ปๅ…ฅใ€‚" _permissions: - "read:account": "ๆŸฅ็œ‹ๅธณๆˆถไฟกๆฏ" - "write:account": "ๆ›ดๆ”นๅธณๆˆถไฟกๆฏ" + "read:account": "ๆŸฅ็œ‹ๆˆ‘็š„ๅธณๆˆถ่ณ‡่จŠ" + "write:account": "ๆ›ดๆ”นๆˆ‘็š„ๅธณๆˆถ่ณ‡่จŠ" "read:blocks": "ๅทฒๅฐ้Ž–็”จๆˆถๅๅ–ฎ" "write:blocks": "็ทจ่ผฏๅทฒๅฐ้Ž–็”จๆˆถๅๅ–ฎ" "read:drive": "ๅญ˜ๅ–้›ฒ็ซฏ็กฌ็ขŸ" @@ -1039,6 +1163,10 @@ _permissions: "write:user-groups": "็ทจ่ผฏไฝฟ็”จ่€…็พค็ต„" "read:channels": "ๅทฒๆŸฅ็œ‹็š„้ ป้“" "write:channels": "็ทจ่ผฏ้ ป้“" + "read:gallery": "็€่ฆฝๅœ–ๅบซ" + "write:gallery": "ๆ“ไฝœๅœ–ๅบซ" + "read:gallery-likes": "่ฎ€ๅ–ๅ–œๆญก็š„ๅœ–็‰‡" + "write:gallery-likes": "ๆ“ไฝœๅ–œๆญก็š„ๅœ–็‰‡" _auth: shareAccess: "่ฆๆŽˆๆฌŠใ€Œโ€œ{name}โ€ใ€ๅญ˜ๅ–ๆ‚จ็š„ๅธณๆˆถๅ—Ž๏ผŸ" shareAccessAsk: "ๆ‚จ็ขบๅฎš่ฆๆŽˆๆฌŠ้€™ๅ€‹ๆ‡‰็”จ็จ‹ๅผไฝฟ็”จๆ‚จ็š„ๅธณๆˆถๅ—Ž๏ผŸ" @@ -1078,6 +1206,8 @@ _widgets: onlineUsers: "็ทšไธŠ็š„็”จๆˆถ" jobQueue: "ไฝ‡ๅˆ—" serverMetric: "ๆœๅ‹™ๅ™จๆŒ‡ๆจ™ " + aiscript: "AiScriptๆŽงๅˆถๅฐ" + aichan: "ๅฐ่—" _cw: hide: "้šฑ่—" show: "็€่ฆฝๆ›ดๅคš" @@ -1103,12 +1233,15 @@ _poll: closed: "ๅทฒ็ตๆŸ" remainingDays: "{d}ๅคฉ{h}ๅฐๆ™‚ๅพŒ็ตๆŸ" remainingHours: "{h}ๅฐๆ™‚{m}ๅˆ†ๅพŒ็ตๆŸ" + remainingMinutes: "{m}ๅˆ†{s}็ง’ๅพŒ็ตๆŸ" remainingSeconds: "{s}็ง’ๅพŒๆˆชๆญข" _visibility: public: "ๅ…ฌ้–‹" publicDescription: "็™ผๅธƒ็ตฆๆ‰€ๆœ‰็”จๆˆถ " home: "้ฆ–้ " + homeDescription: "ๅƒ…็™ผ้€่‡ณ้ฆ–้ ็š„ๆ™‚้–“่ปธ" followers: "่ฟฝ้šจ่€…" + followersDescription: "ๅƒ…็™ผ้€่‡ณ้—œๆณจ่€…" specified: "ๆŒ‡ๅฎšไฝฟ็”จ่€…" specifiedDescription: "ๅƒ…็™ผ้€่‡ณๆŒ‡ๅฎšไฝฟ็”จ่€…" localOnly: "ๅƒ…้™ๆœฌๅœฐ" @@ -1131,6 +1264,7 @@ _profile: youCanIncludeHashtags: "ไฝ ไนŸๅฏไปฅๅœจใ€Œ้—œๆ–ผๆˆ‘ใ€ไธญๅŠ ไธŠ #tag" metadata: "้€ฒ้šŽ่ณ‡่จŠ" metadataEdit: "็ทจ่ผฏ้€ฒ้šŽ่ณ‡่จŠ" + metadataDescription: "ๅฏไปฅๅœจๅ€‹ไบบ่ณ‡ๆ–™ไธญไปฅ่กจๆ ผๅฝขๅผ้กฏ็คบๅ…ถไป–่ณ‡่จŠใ€‚" metadataLabel: "ๆจ™็ฑค" metadataContent: "ๅ†…ๅฎน" changeAvatar: "ๆ›ดๆ›ๅคง้ ญ่ฒผ" @@ -1141,6 +1275,8 @@ _exportOrImport: muteList: "้œ้Ÿณ" blockingList: "ๅฐ้Ž–" userLists: "ๆธ…ๅ–ฎ" + excludeMutingUsers: "ๆŽ’้™ค่ขซ้œ้Ÿณ็š„็”จๆˆถ" + excludeInactiveUsers: "ๆŽ’้™คไธๆดป่บๅธณๆˆถ" _charts: federation: "็ซ™ๅฐ่ฏ้‚ฆ" apRequest: "่ซ‹ๆฑ‚" @@ -1418,6 +1554,7 @@ _pages: _seedRandomPick: arg1: "็จฎๅญ" arg2: "ๆธ…ๅ–ฎ" + DRPWPM: "ไปŽๆฉŸ็Ž‡ๅˆ—่กจไธญ้šจๆฉŸ้ธๆ“‡๏ผˆๆฏๅ€‹็”จๆˆทๆฏๅคฉ๏ผ‰" _DRPWPM: arg1: "ๅญ—ไธฒไธฒๅˆ—" pick: "ๅพžๆธ…ๅ–ฎไธญ้ธๅ–" @@ -1448,6 +1585,8 @@ _pages: _for: arg1: "้‡่ค‡ๆฌกๆ•ธ" arg2: "่™•็†" + typeError: "ๆงฝๅƒๆ•ธ{slot}้œ€่ฆๅ‚ณๅ…ฅโ€œ{expect}โ€๏ผŒไฝ†ๆ˜ฏๅฏฆ้š›ๅ‚ณๅ…ฅ็‚บโ€œ{actual}โ€๏ผ" + thereIsEmptySlot: "ๅƒๆ•ธ{slot}ๆ˜ฏ็ฉบ็š„๏ผ" types: string: "ๅญ—ไธฒ" number: "ๆ•ฐๅ€ผ" @@ -1470,10 +1609,13 @@ _notification: youRenoted: "{name} ่ฝ‰็™ผไบ†ไฝ ็š„่ฒผๆ–‡" youGotPoll: "{name}ๅทฒๆŠ•็ฅจ" youGotMessagingMessageFromUser: "{name}็™ผ้€็ตฆๆ‚จ็š„่จŠๆฏ" + youGotMessagingMessageFromGroup: "{name}็™ผ้€็ตฆๆ‚จ็š„่จŠๆฏ" youWereFollowed: "ๆ‚จๆœ‰ๆ–ฐ็š„่ฟฝ้šจ่€…" youReceivedFollowRequest: "ๆ‚จๆœ‰ๆ–ฐ็š„่ฟฝ้šจ่ซ‹ๆฑ‚" yourFollowRequestAccepted: "ๆ‚จ็š„่ฟฝ้šจ่ซ‹ๆฑ‚ๅทฒ้€š้Ž" youWereInvitedToGroup: "ๆ‚จๆœ‰ๆ–ฐ็š„็พค็ต„้‚€่ซ‹" + pollEnded: "ๅ•ๅท่ชฟๆŸฅๅทฒ็”ข็”Ÿ็ตๆžœ" + emptyPushNotificationMessage: "ๆŽจ้€้€š็Ÿฅๅทฒๆ›ดๆ–ฐ" _types: all: "ๅ…จ้ƒจ " follow: "่ฟฝ้šจไธญ" @@ -1483,10 +1625,15 @@ _notification: quote: "ๅผ•็”จ" reaction: "ๅๆ‡‰" pollVote: "็ตฑ่จˆๅทฒๆŠ•็ฅจๆ•ธ" + pollEnded: "ๅ•ๅท่ชฟๆŸฅ็ตๆŸ" receiveFollowRequest: "ๅทฒๆ”ถๅˆฐ่ฟฝ้šจ่ซ‹ๆฑ‚" followRequestAccepted: "่ฟฝ้šจ่ซ‹ๆฑ‚ๅทฒๆŽฅๅ—" groupInvited: "ๅŠ ๅ…ฅ็คพ็พค้‚€่ซ‹" app: "ๆ‡‰็”จ็จ‹ๅผ้€š็Ÿฅ" + _actions: + followBack: "ๅ›ž้—œ" + reply: "ๅ›ž่ฆ†" + renote: "่ฝ‰็™ผ" _deck: alwaysShowMainColumn: "็ธฝๆ˜ฏ้กฏ็คบไธปๆฌ„" columnAlign: "ๅฐ้ฝŠๆฌ„ไฝ" diff --git a/package.json b/package.json index 606e2b7332..e3340005ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "12.110.1", + "version": "12.111.0", "codename": "indigo", "repository": { "type": "git", @@ -19,10 +19,10 @@ "watch": "npm run dev", "dev": "node ./scripts/dev.js", "lint": "node ./scripts/lint.js", - "cy:open": "cypress open", + "cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "cypress run", "e2e": "start-server-and-test start:test http://localhost:61812 cy:run", - "mocha": "cd packages/backend && cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" npx mocha", + "mocha": "cd packages/backend && cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" npx mocha", "test": "npm run mocha", "format": "gulp format", "clean": "node ./scripts/clean.js", @@ -30,8 +30,6 @@ "cleanall": "npm run clean-all" }, "dependencies": { - "@types/gulp": "4.0.9", - "@types/gulp-rename": "2.0.1", "execa": "5.1.1", "gulp": "4.0.2", "gulp-cssnano": "2.1.3", @@ -41,10 +39,12 @@ "js-yaml": "4.1.0" }, "devDependencies": { - "@typescript-eslint/parser": "5.18.0", + "@types/gulp": "4.0.9", + "@types/gulp-rename": "2.0.1", + "@typescript-eslint/parser": "5.27.1", "cross-env": "7.0.3", - "cypress": "9.5.3", + "cypress": "10.0.3", "start-server-and-test": "1.14.0", - "typescript": "4.6.3" + "typescript": "4.7.3" } } diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs index e2e31e9e33..5a06889dcd 100644 --- a/packages/backend/.eslintrc.cjs +++ b/packages/backend/.eslintrc.cjs @@ -6,4 +6,27 @@ module.exports = { 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/.mocharc.json b/packages/backend/.mocharc.json index 26628066eb..87c571cfd6 100644 --- a/packages/backend/.mocharc.json +++ b/packages/backend/.mocharc.json @@ -5,6 +5,6 @@ "loader=./test/loader.js" ], "slow": 1000, - "timeout": 35000, + "timeout": 10000, "exit": true } diff --git a/packages/backend/.vscode/settings.json b/packages/backend/.vscode/settings.json index df3bf05071..9fb3b29d4a 100644 --- a/packages/backend/.vscode/settings.json +++ b/packages/backend/.vscode/settings.json @@ -2,5 +2,9 @@ "typescript.tsdk": "node_modules\\typescript\\lib", "path-intellisense.mappings": { "@": "${workspaceRoot}/packages/backend/src/" + }, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true } } diff --git a/packages/backend/migration/1651224615271-foreign-key.js b/packages/backend/migration/1651224615271-foreign-key.js new file mode 100644 index 0000000000..44ba7fb6c4 --- /dev/null +++ b/packages/backend/migration/1651224615271-foreign-key.js @@ -0,0 +1,89 @@ +export class foreignKeyReports1651224615271 { + name = 'foreignKeyReports1651224615271' + + async up(queryRunner) { + await Promise.all([ + queryRunner.query(`ALTER INDEX "public"."IDX_seoignmeoprigmkpodgrjmkpormg" RENAME TO "IDX_c8cc87bd0f2f4487d17c651fbf"`), + queryRunner.query(`DROP INDEX "public"."IDX_note_on_channelId_and_id_desc"`), + + // remove unnecessary default null, see also down + queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "followersUri" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "session" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "name" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "description" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "iconUrl" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareName" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareVersion" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "name" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "description" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerName" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerEmail" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "iconUrl" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "faviconUrl" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "themeColor" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "clip" ALTER COLUMN "description" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "channelId" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" DROP DEFAULT`), + + queryRunner.query(`CREATE INDEX "IDX_315c779174fe8247ab324f036e" ON "drive_file" ("isLink")`), + queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId")`), + queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`), + + queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`).then(() => { + queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + }), + + queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`), + queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`), + queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9" UNIQUE ("userId")`), + queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`), + queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c" UNIQUE ("noteId")`), + + queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" TO "FK_a9ca79ad939bf06066b81c9d3aa"`), + + queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" ADD VALUE 'pollEnded' AFTER 'pollVote'`), + ]); + } + + async down(queryRunner) { + await Promise.all([ + // There is no ALTER TYPE REMOVE VALUE query, so the reverse operation is a bit more complex + queryRunner.query(`UPDATE "user_profile" SET "mutingNotificationTypes" = array_remove("mutingNotificationTypes", 'pollEnded')`) + .then(() => + queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`) + ).then(() => + queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`) + ).then(() => + queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`) + ).then(() => + queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`) + ).then(() => + queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`) + ).then(() => + queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`) + ), + + queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" TO "FK_3126dd7c502c9e4d7597ef7ef10"`), + + queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c"`), + queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`), + queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9"`), + queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6"`), + queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b"`), + + queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" SET DEFAULT '{}'`), + queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`), + + queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`), + queryRunner.query(`DROP INDEX "public"."IDX_f22169eb10657bded6d875ac8f"`), + queryRunner.query(`DROP INDEX "public"."IDX_315c779174fe8247ab324f036e"`), + + /* DEFAULT's are not set again because if the column can be NULL, then DEFAULT NULL is not necessary. + see also https://github.com/typeorm/typeorm/issues/7579#issuecomment-835423615 */ + + queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("id", "channelId") `), + queryRunner.query(`ALTER INDEX "public"."IDX_c8cc87bd0f2f4487d17c651fbf" RENAME TO "IDX_seoignmeoprigmkpodgrjmkpormg"`), + ]); + } +} diff --git a/packages/backend/migration/1652859567549-uniform-themecolor.js b/packages/backend/migration/1652859567549-uniform-themecolor.js new file mode 100644 index 0000000000..8da1fd7fbb --- /dev/null +++ b/packages/backend/migration/1652859567549-uniform-themecolor.js @@ -0,0 +1,36 @@ +import tinycolor from 'tinycolor2'; + +export class uniformThemecolor1652859567549 { + name = 'uniformThemecolor1652859567549' + + async up(queryRunner) { + const formatColor = (color) => { + let tc = new tinycolor(color); + if (tc.isValid()) { + return tc.toHexString(); + } else { + return null; + } + }; + + await queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL') + .then(instances => Promise.all(instances.map(instance => { + // update theme color to uniform format, e.g. #00ff00 + // invalid theme colors get set to null + return queryRunner.query('UPDATE "instance" SET "themeColor" = $1 WHERE "id" = $2', [formatColor(instance.themeColor), instance.id]); + }))); + + // also fix own theme color + await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1') + .then(metas => { + if (metas.length > 0) { + return queryRunner.query('UPDATE "meta" SET "themeColor" = $1', [formatColor(metas[0].themeColor)]); + } + }); + } + + async down(queryRunner) { + // The original representation is not stored, so migrating back is not possible. + // The new format also works in older versions so this is not a problem. + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 314818f80b..2186dcc6a9 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -6,7 +6,7 @@ "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", "lint": "eslint --quiet \"src/**/*.ts\"", - "mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", + "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", "test": "npm run mocha" }, "resolutions": { @@ -14,71 +14,25 @@ "lodash": "^4.17.21" }, "dependencies": { - "@discordapp/twemoji": "13.1.1", + "@bull-board/koa": "3.11.1", + "@discordapp/twemoji": "14.0.2", "@elastic/elasticsearch": "7.11.0", "@koa/cors": "3.1.0", "@koa/multer": "3.0.0", "@koa/router": "9.0.1", - "@sinonjs/fake-timers": "9.1.1", + "@peertube/http-signature": "1.6.0", + "@sinonjs/fake-timers": "9.1.2", "@syuilo/aiscript": "0.11.1", - "@types/bcryptjs": "2.4.2", - "@types/bull": "3.15.8", - "@types/cbor": "6.0.0", - "@types/escape-regexp": "0.0.1", - "@types/is-url": "1.2.30", - "@types/js-yaml": "4.0.5", - "@types/jsdom": "16.2.14", - "@types/jsonld": "1.5.6", - "@types/koa": "2.13.4", - "@types/koa-bodyparser": "4.3.7", - "@types/koa-cors": "0.0.2", - "@types/koa-favicon": "2.0.21", - "@types/koa-logger": "3.1.2", - "@types/koa-mount": "4.0.1", - "@types/koa-send": "4.1.3", - "@types/koa-views": "7.0.0", - "@types/koa__cors": "3.1.1", - "@types/koa__multer": "2.0.4", - "@types/koa__router": "8.0.11", - "@types/mocha": "9.1.0", - "@types/node": "17.0.23", - "@types/node-fetch": "3.0.3", - "@types/nodemailer": "6.4.4", - "@types/oauth": "0.9.1", - "@types/parse5": "6.0.3", - "@types/portscanner": "2.1.1", - "@types/pug": "2.0.6", - "@types/punycode": "2.1.0", - "@types/qrcode": "1.4.2", - "@types/random-seed": "0.3.3", - "@types/ratelimiter": "3.4.3", - "@types/redis": "4.0.11", - "@types/rename": "1.0.4", - "@types/sanitize-html": "2.6.2", - "@types/sharp": "0.30.1", - "@types/sinonjs__fake-timers": "8.1.2", - "@types/speakeasy": "2.0.7", - "@types/tinycolor2": "1.4.3", - "@types/tmp": "0.2.3", - "@types/uuid": "8.3.4", - "@types/web-push": "3.3.2", - "@types/websocket": "1.0.5", - "@types/ws": "8.5.3", - "@typescript-eslint/eslint-plugin": "5.18.0", - "@typescript-eslint/parser": "5.18.0", - "@bull-board/koa": "3.10.3", "abort-controller": "3.0.0", "ajv": "8.11.0", - "archiver": "5.3.0", + "archiver": "5.3.1", "autobind-decorator": "2.4.0", "autwh": "0.1.0", - "aws-sdk": "2.1111.0", + "aws-sdk": "2.1152.0", "bcryptjs": "2.4.3", "blurhash": "1.1.5", - "broadcast-channel": "4.10.0", - "bull": "4.8.1", + "bull": "4.8.3", "cacheable-lookup": "6.0.4", - "cafy": "15.2.1", "cbor": "8.1.0", "chalk": "5.0.1", "chalk-template": "0.4.0", @@ -88,22 +42,19 @@ "date-fns": "2.28.0", "deep-email-validator": "0.1.21", "escape-regexp": "0.0.1", - "eslint": "8.13.0", - "eslint-plugin-import": "2.26.0", "feed": "4.2.2", - "file-type": "17.1.1", + "file-type": "17.1.2", "fluent-ffmpeg": "2.1.2", - "got": "12.0.3", + "got": "12.1.0", "hpagent": "0.1.2", - "http-signature": "1.3.6", - "ip-cidr": "3.0.4", + "ip-cidr": "3.0.10", "is-svg": "4.3.2", "js-yaml": "4.1.0", "jsdom": "19.0.0", "json5": "2.2.1", "json5-loader": "4.0.1", - "jsonld": "5.2.0", - "jsrsasign": "8.0.20", + "jsonld": "6.0.0", + "jsrsasign": "10.5.24", "koa": "2.13.4", "koa-bodyparser": "4.3.0", "koa-favicon": "2.1.0", @@ -113,19 +64,18 @@ "koa-send": "5.0.1", "koa-slow": "2.1.0", "koa-views": "7.0.2", - "mfm-js": "0.21.0", + "mfm-js": "0.22.1", "mime-types": "2.1.35", "misskey-js": "0.0.14", - "mocha": "9.2.2", + "mocha": "10.0.0", "ms": "3.0.0-canary.1", "multer": "1.4.4", "nested-property": "4.0.0", - "node-fetch": "3.2.3", - "nodemailer": "6.7.3", + "node-fetch": "3.2.6", + "nodemailer": "6.7.5", "os-utils": "0.0.14", "parse5": "6.0.1", "pg": "8.7.3", - "portscanner": "2.2.0", "private-ip": "2.3.3", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -144,35 +94,83 @@ "rndstr": "1.0.0", "s-age": "1.1.2", "sanitize-html": "2.7.0", - "semver": "7.3.6", - "sharp": "0.30.3", + "semver": "7.3.7", + "sharp": "0.29.3", "speakeasy": "2.0.0", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "style-loader": "3.3.1", - "summaly": "2.5.0", + "summaly": "2.5.1", "syslog-pro": "1.0.0", - "systeminformation": "5.11.9", + "systeminformation": "5.11.16", "tinycolor2": "1.4.2", "tmp": "0.2.1", - "ts-loader": "9.2.8", - "ts-node": "10.7.0", - "tsc-alias": "1.4.1", - "tsconfig-paths": "3.14.1", + "ts-loader": "9.3.0", + "ts-node": "10.8.1", + "tsc-alias": "1.6.9", + "tsconfig-paths": "4.0.0", "twemoji-parser": "14.0.0", - "typeorm": "0.3.5", - "typescript": "4.6.3", + "typeorm": "0.3.6", "ulid": "2.3.0", "unzipper": "0.10.11", "uuid": "8.3.2", - "web-push": "3.4.5", + "web-push": "3.5.0", "websocket": "1.0.34", - "ws": "8.5.0", - "xev": "2.0.1" + "ws": "8.8.0", + "xev": "3.0.2" }, "devDependencies": { - "@redocly/openapi-core": "1.0.0-beta.93", + "@redocly/openapi-core": "1.0.0-beta.97", + "@types/semver": "7.3.9", + "@types/bcryptjs": "2.4.2", + "@types/bull": "3.15.8", + "@types/cbor": "6.0.0", + "@types/escape-regexp": "0.0.1", "@types/fluent-ffmpeg": "2.1.20", + "@types/is-url": "1.2.30", + "@types/js-yaml": "4.0.5", + "@types/jsdom": "16.2.14", + "@types/jsonld": "1.5.6", + "@types/jsrsasign": "10.5.1", + "@types/koa": "2.13.4", + "@types/koa-bodyparser": "4.3.7", + "@types/koa-cors": "0.0.2", + "@types/koa-favicon": "2.0.21", + "@types/koa-logger": "3.1.2", + "@types/koa-mount": "4.0.1", + "@types/koa-send": "4.1.3", + "@types/koa-views": "7.0.0", + "@types/koa__cors": "3.1.1", + "@types/koa__multer": "2.0.4", + "@types/koa__router": "8.0.11", + "@types/mocha": "9.1.1", + "@types/node": "17.0.41", + "@types/node-fetch": "3.0.3", + "@types/nodemailer": "6.4.4", + "@types/oauth": "0.9.1", + "@types/parse5": "6.0.3", + "@types/pug": "2.0.6", + "@types/punycode": "2.1.0", + "@types/qrcode": "1.4.2", + "@types/random-seed": "0.3.3", + "@types/ratelimiter": "3.4.3", + "@types/redis": "4.0.11", + "@types/rename": "1.0.4", + "@types/sanitize-html": "2.6.2", + "@types/sharp": "0.30.2", + "@types/sinonjs__fake-timers": "8.1.2", + "@types/speakeasy": "2.0.7", + "@types/tinycolor2": "1.4.3", + "@types/tmp": "0.2.3", + "@types/uuid": "8.3.4", + "@types/web-push": "3.3.2", + "@types/websocket": "1.0.5", + "@types/ws": "8.5.3", + "@typescript-eslint/eslint-plugin": "5.27.1", + "@typescript-eslint/parser": "5.27.1", + "typescript": "4.7.3", + "eslint": "8.17.0", + "eslint-plugin-import": "2.26.0", "cross-env": "7.0.3", "execa": "6.1.0" } diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts index 8d484312dc..d1f9cd9552 100644 --- a/packages/backend/src/@types/http-signature.d.ts +++ b/packages/backend/src/@types/http-signature.d.ts @@ -1,5 +1,5 @@ -declare module 'http-signature' { - import { IncomingMessage, ClientRequest } from 'http'; +declare module '@peertube/http-signature' { + import { IncomingMessage, ClientRequest } from 'node:http'; interface ISignature { keyId: string; diff --git a/packages/backend/src/@types/jsrsasign.d.ts b/packages/backend/src/@types/jsrsasign.d.ts deleted file mode 100644 index bb52f8f64e..0000000000 --- a/packages/backend/src/@types/jsrsasign.d.ts +++ /dev/null @@ -1,800 +0,0 @@ -// Attention: Partial Type Definition - -declare module 'jsrsasign' { - //// HELPER TYPES - - /** - * Attention: The value might be changed by the function. - */ - type Mutable = T; - - /** - * Deprecated: The function might be deleted in future release. - */ - type Deprecated = T; - - //// COMMON TYPES - - /** - * byte number - */ - type ByteNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255; - - /** - * hexadecimal string /[0-9A-F]/ - */ - type HexString = string; - - /** - * binary string /[01]/ - */ - type BinString = string; - - /** - * base64 string /[A-Za-z0-9+/]=+/ - */ - type Base64String = string; - - /** - * base64 URL encoded string /[A-Za-z0-9_-]/ - */ - type Base64URLString = string; - - /** - * time value (ex. "151231235959Z") - */ - type TimeValue = string; - - /** - * OID string (ex. '1.2.3.4.567') - */ - type OID = string; - - /** - * OID name - */ - type OIDName = string; - - /** - * PEM formatted string - */ - type PEM = string; - - //// ASN1 TYPES - - class ASN1Object { - public isModified: boolean; - - public hTLV: ASN1TLV; - - public hT: ASN1T; - - public hL: ASN1L; - - public hV: ASN1V; - - public getLengthHexFromValue(): HexString; - - public getEncodedHex(): ASN1TLV; - - public getValueHex(): ASN1V; - - public getFreshValueHex(): ASN1V; - } - - class DERAbstractStructured extends ASN1Object { - constructor(params?: Partial>); - - public setByASN1ObjectArray(asn1ObjectArray: ASN1Object[]): void; - - public appendASN1Object(asn1Object: ASN1Object): void; - } - - class DERSequence extends DERAbstractStructured { - constructor(params?: Partial>); - - public getFreshValueHex(): ASN1V; - } - - //// ASN1HEX TYPES - - /** - * ASN.1 DER encoded data (hexadecimal string) - */ - type ASN1S = HexString; - - /** - * index of something - */ - type Idx = ASN1S extends { [idx: string]: unknown } ? string : ASN1S extends { [idx: number]: unknown } ? number : never; - - /** - * byte length of something - */ - type ByteLength = T['length']; - - /** - * ASN.1 L(length) (hexadecimal string) - */ - type ASN1L = HexString; - - /** - * ASN.1 T(tag) (hexadecimal string) - */ - type ASN1T = HexString; - - /** - * ASN.1 V(value) (hexadecimal string) - */ - type ASN1V = HexString; - - /** - * ASN.1 TLV (hexadecimal string) - */ - type ASN1TLV = HexString; - - /** - * ASN.1 object string - */ - type ASN1ObjectString = string; - - /** - * nth - */ - type Nth = number; - - /** - * ASN.1 DER encoded OID value (hexadecimal string) - */ - type ASN1OIDV = HexString; - - class ASN1HEX { - public static getLblen(s: ASN1S, idx: Idx): ByteLength; - - public static getL(s: ASN1S, idx: Idx): ASN1L; - - public static getVblen(s: ASN1S, idx: Idx): ByteLength; - - public static getVidx(s: ASN1S, idx: Idx): Idx; - - public static getV(s: ASN1S, idx: Idx): ASN1V; - - public static getTLV(s: ASN1S, idx: Idx): ASN1TLV; - - public static getNextSiblingIdx(s: ASN1S, idx: Idx): Idx; - - public static getChildIdx(h: ASN1S, pos: Idx): Idx[]; - - public static getNthChildIdx(h: ASN1S, idx: Idx, nth: Nth): Idx; - - public static getIdxbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string): Idx>; - - public static getTLVbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string): ASN1TLV; - - // eslint:disable-next-line:bool-param-default - public static getVbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string, removeUnusedbits?: boolean): ASN1V; - - public static hextooidstr(hex: ASN1OIDV): OID; - - public static dump(hexOrObj: ASN1S | ASN1Object, flags?: Record, idx?: Idx, indent?: string): string; - - public static isASN1HEX(hex: string): hex is HexString; - - public static oidname(oidDotOrHex: OID | ASN1OIDV): OIDName; - } - - //// BIG INTEGER TYPES (PARTIAL) - - class BigInteger { - constructor(a: null); - - constructor(a: number, b: SecureRandom); - - constructor(a: number, b: number, c: SecureRandom); - - constructor(a: unknown); - - constructor(a: string, b: number); - - public am(i: number, x: number, w: number, j: number, c: number, n: number): number; - - public DB: number; - - public DM: number; - - public DV: number; - - public FV: number; - - public F1: number; - - public F2: number; - - protected copyTo(r: Mutable): void; - - protected fromInt(x: number): void; - - protected fromString(s: string, b: number): void; - - protected clamp(): void; - - public toString(b: number): string; - - public negate(): BigInteger; - - public abs(): BigInteger; - - public compareTo(a: BigInteger): number; - - public bitLength(): number; - - protected dlShiftTo(n: number, r: Mutable): void; - - protected drShiftTo(n: number, r: Mutable): void; - - protected lShiftTo(n: number, r: Mutable): void; - - protected rShiftTo(n: number, r: Mutable): void; - - protected subTo(a: BigInteger, r: Mutable): void; - - protected multiplyTo(a: BigInteger, r: Mutable): void; - - protected squareTo(r: Mutable): void; - - protected divRemTo(m: BigInteger, q: Mutable, r: Mutable): void; - - public mod(a: BigInteger): BigInteger; - - protected invDigit(): number; - - protected isEven(): boolean; - - protected exp(e: number, z: Classic | Montgomery): BigInteger; - - public modPowInt(e: number, m: BigInteger): BigInteger; - - public static ZERO: BigInteger; - - public static ONE: BigInteger; - } - - class Classic { - constructor(m: BigInteger); - - public convert(x: BigInteger): BigInteger; - - public revert(x: BigInteger): BigInteger; - - public reduce(x: Mutable): void; - - public mulTo(x: BigInteger, r: Mutable): void; - - public sqrTo(x: BigInteger, y: BigInteger, r: Mutable): void; - } - - class Montgomery { - constructor(m: BigInteger); - - public convert(x: BigInteger): BigInteger; - - public revert(x: BigInteger): BigInteger; - - public reduce(x: Mutable): void; - - public mulTo(x: BigInteger, r: Mutable): void; - - public sqrTo(x: BigInteger, y: BigInteger, r: Mutable): void; - } - - //// KEYUTIL TYPES - - type DecryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type Decrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type DecryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type EncryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type Encrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type EncryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type AlgList = { - 'AES-256-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 32; ivlen: 16; }; - 'AES-192-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 24; ivlen: 16; }; - 'AES-128-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 16; ivlen: 16; }; - 'DES-EDE3-CBC': { 'proc': Decrypt3DES; 'eproc': Encrypt3DES; keylen: 24; ivlen: 8; }; - 'DES-CBC': { 'proc': DecryptDES; 'eproc': EncryptDES; keylen: 8; ivlen: 8; }; - }; - - type AlgName = keyof AlgList; - - type PEMHeadAlgName = 'RSA' | 'EC' | 'DSA'; - - type GetKeyRSAParam = RSAKey | { - n: BigInteger; - e: number; - } | Record<'n' | 'e', HexString> | Record<'n' | 'e', HexString> & Record<'d' | 'p' | 'q' | 'dp' | 'dq' | 'co', HexString | null> | { - n: BigInteger; - e: number; - d: BigInteger; - } | { - kty: 'RSA'; - } & Record<'n' | 'e', Base64URLString> | { - kty: 'RSA'; - } & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | { - kty: 'RSA'; - } & Record<'n' | 'e' | 'd', Base64URLString>; - - type GetKeyECDSAParam = KJUR.crypto.ECDSA | { - curve: KJUR.crypto.CurveName; - xy: HexString; - } | { - curve: KJUR.crypto.CurveName; - d: HexString; - } | { - kty: 'EC'; - crv: KJUR.crypto.CurveName; - x: Base64URLString; - y: Base64URLString; - } | { - kty: 'EC'; - crv: KJUR.crypto.CurveName; - x: Base64URLString; - y: Base64URLString; - d: Base64URLString; - }; - - type GetKeyDSAParam = KJUR.crypto.DSA | Record<'p' | 'q' | 'g', BigInteger> & Record<'y', BigInteger | null> | Record<'p' | 'q' | 'g' | 'x', BigInteger> & Record<'y', BigInteger | null>; - - type GetKeyParam = GetKeyRSAParam | GetKeyECDSAParam | GetKeyDSAParam | string; - - class KEYUTIL { - public version: '1.0.0'; - - public parsePKCS5PEM(sPKCS5PEM: PEM): Partial> & (Record<'cipher' | 'ivsalt', string> | Record<'cipher' | 'ivsalt', undefined>); - - public getKeyAndUnusedIvByPasscodeAndIvsalt(algName: AlgName, passcode: string, ivsaltHex: HexString): Record<'keyhex' | 'ivhex', HexString>; - - public decryptKeyB64(privateKeyB64: Base64String, sharedKeyAlgName: AlgName, sharedKeyHex: HexString, ivsaltHex: HexString): Base64String; - - public getDecryptedKeyHex(sEncryptedPEM: PEM, passcode: string): HexString; - - public getEncryptedPKCS5PEMFromPrvKeyHex(pemHeadAlg: PEMHeadAlgName, hPrvKey: string, passcode: string, sharedKeyAlgName?: AlgName | null, ivsaltHex?: HexString | null): PEM; - - public parseHexOfEncryptedPKCS8(sHEX: HexString): { - ciphertext: ASN1V; - encryptionSchemeAlg: 'TripleDES'; - encryptionSchemeIV: ASN1V; - pbkdf2Salt: ASN1V; - pbkdf2Iter: number; - }; - - public getPBKDF2KeyHexFromParam(info: ReturnType, passcode: string): HexString; - - private _getPlainPKCS8HexFromEncryptedPKCS8PEM(pkcs8PEM: PEM, passcode: string): HexString; - - public getKeyFromEncryptedPKCS8PEM(prvKeyHex: HexString): ReturnType; - - public parsePlainPrivatePKCS8Hex(pkcs8PrvHex: HexString): { - algparam: ASN1V | null; - algoid: ASN1V; - keyidx: Idx; - }; - - public getKeyFromPlainPrivatePKCS8PEM(prvKeyHex: HexString): ReturnType; - - public getKeyFromPlainPrivatePKCS8Hex(prvKeyHex: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA; - - private _getKeyFromPublicPKCS8Hex(h: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA; - - public parsePublicRawRSAKeyHex(pubRawRSAHex: HexString): Record<'n' | 'e', ASN1V>; - - public parsePublicPKCS8Hex(pkcs8PubHex: HexString): { - algparam: ASN1V | Record<'p' | 'q' | 'g', ASN1V> | null; - algoid: ASN1V; - key: ASN1V; - }; - - public static getKey(param: GetKeyRSAParam): RSAKey; - - public static getKey(param: GetKeyECDSAParam): KJUR.crypto.ECDSA; - - public static getKey(param: GetKeyDSAParam): KJUR.crypto.DSA; - - public static getKey(param: string, passcode?: string, hextype?: string): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public static generateKeypair(alg: 'RSA', keylen: number): Record<'prvKeyObj' | 'pubKeyObj', RSAKey>; - - public static generateKeypair(alg: 'EC', curve: KJUR.crypto.CurveName): Record<'prvKeyObj' | 'pubKeyObj', KJUR.crypto.ECDSA>; - - public static getPEM(keyObjOrHex: RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA, formatType?: 'PKCS1PRV' | 'PKCS5PRV' | 'PKCS8PRV', passwd?: string, encAlg?: 'DES-CBC' | 'DES-EDE3-CBC' | 'AES-128-CBC' | 'AES-192-CBC' | 'AES-256-CBC', hexType?: string, ivsaltHex?: HexString): object; // To Do - - public static getKeyFromCSRPEM(csrPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public static getKeyFromCSRHex(csrHex: HexString): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public static parseCSRHex(csrHex: HexString): Record<'p8pubkeyhex', ASN1TLV>; - - public static getJWKFromKey(keyObj: RSAKey): { - kty: 'RSA'; - } & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | { - kty: 'RSA'; - } & Record<'n' | 'e', Base64URLString>; - - public static getJWKFromKey(keyObj: KJUR.crypto.ECDSA): { - kty: 'EC'; - crv: KJUR.crypto.CurveName; - x: Base64URLString; - y: Base64URLString; - d: Base64URLString; - } | { - kty: 'EC'; - crv: KJUR.crypto.CurveName; - x: Base64URLString; - y: Base64URLString; - }; - } - - //// KJUR NAMESPACE (PARTIAL) - - namespace KJUR { - namespace crypto { - type CurveName = 'secp128r1' | 'secp160k1' | 'secp160r1' | 'secp192k1' | 'secp192r1' | 'secp224r1' | 'secp256k1' | 'secp256r1' | 'secp384r1' | 'secp521r1'; - - class DSA { - public p: BigInteger | null; - - public q: BigInteger | null; - - public g: BigInteger | null; - - public y: BigInteger | null; - - public x: BigInteger | null; - - public type: 'DSA'; - - public isPrivate: boolean; - - public isPublic: boolean; - - public setPrivate(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger | null, x: BigInteger): void; - - public setPrivateHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString | null, hX: HexString): void; - - public setPublic(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger): void; - - public setPublicHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString): void; - - public signWithMessageHash(sHashHex: HexString): HexString; - - public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean; - - public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger]; - - public readPKCS5PrvKeyHex(h: HexString): void; - - public readPKCS8PrvKeyHex(h: HexString): void; - - public readPKCS8PubKeyHex(h: HexString): void; - - public readCertPubKeyHex(h: HexString, nthPKI: number): void; - } - - class ECDSA { - constructor(params?: { - curve?: CurveName; - prv?: HexString; - pub?: HexString; - }); - - public p: BigInteger | null; - - public q: BigInteger | null; - - public g: BigInteger | null; - - public y: BigInteger | null; - - public x: BigInteger | null; - - public type: 'EC'; - - public isPrivate: boolean; - - public isPublic: boolean; - - public getBigRandom(limit: BigInteger): BigInteger; - - public setNamedCurve(curveName: CurveName): void; - - public setPrivateKeyHex(prvKeyHex: HexString): void; - - public setPublicKeyHex(pubKeyHex: HexString): void; - - public getPublicKeyXYHex(): Record<'x' | 'y', HexString>; - - public getShortNISTPCurveName(): 'P-256' | 'P-384' | null; - - public generateKeyPairHex(): Record<'ecprvhex' | 'ecpubhex', HexString>; - - public signWithMessageHash(hashHex: HexString): HexString; - - public signHex(hashHex: HexString, privHex: HexString): HexString; - - public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean; - - public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger]; - - public readPKCS5PrvKeyHex(h: HexString): void; - - public readPKCS8PrvKeyHex(h: HexString): void; - - public readPKCS8PubKeyHex(h: HexString): void; - - public readCertPubKeyHex(h: HexString, nthPKI: number): void; - - public static parseSigHex(sigHex: HexString): Record<'r' | 's', BigInteger>; - - public static parseSigHexInHexRS(sigHex: HexString): Record<'r' | 's', ASN1V>; - - public static asn1SigToConcatSig(asn1Sig: HexString): HexString; - - public static concatSigToASN1Sig(concatSig: HexString): ASN1TLV; - - public static hexRSSigToASN1Sig(hR: HexString, hS: HexString): ASN1TLV; - - public static biRSSigToASN1Sig(biR: BigInteger, biS: BigInteger): ASN1TLV; - - public static getName(s: CurveName | HexString): 'secp256r1' | 'secp256k1' | 'secp384r1' | null; - } - - class Signature { - constructor(params?: ({ - alg: string; - prov?: string; - } | {}) & ({ - psssaltlen: number; - } | {}) & ({ - prvkeypem: PEM; - prvkeypas?: never; - } | {})); - - private _setAlgNames(): void; - - private _zeroPaddingOfSignature(hex: HexString, bitLength: number): HexString; - - public setAlgAndProvider(alg: string, prov: string): void; - - public init(key: GetKeyParam, pass?: string): void; - - public updateString(str: string): void; - - public updateHex(hex: HexString): void; - - public sign(): HexString; - - public signString(str: string): HexString; - - public signHex(hex: HexString): HexString; - - public verify(hSigVal: string): boolean | 0; - } - } - } - - //// RSAKEY TYPES - - class RSAKey { - public n: BigInteger | null; - - public e: number; - - public d: BigInteger | null; - - public p: BigInteger | null; - - public q: BigInteger | null; - - public dmp1: BigInteger | null; - - public dmq1: BigInteger | null; - - public coeff: BigInteger | null; - - public type: 'RSA'; - - public isPrivate?: boolean; - - public isPublic?: boolean; - - //// RSA PUBLIC - - protected doPublic(x: BigInteger): BigInteger; - - public setPublic(N: BigInteger, E: number): void; - - public setPublic(N: HexString, E: HexString): void; - - public encrypt(text: string): HexString | null; - - public encryptOAEP(text: string, hash?: string | ((s: string) => string), hashLen?: number): HexString | null; - - //// RSA PRIVATE - - protected doPrivate(x: BigInteger): BigInteger; - - public setPrivate(N: BigInteger, E: number, D: BigInteger): void; - - public setPrivate(N: HexString, E: HexString, D: HexString): void; - - public setPrivateEx(N: HexString, E: HexString, D?: HexString | null, P?: HexString | null, Q?: HexString | null, DP?: HexString | null, DQ?: HexString | null, C?: HexString | null): void; - - public generate(B: number, E: HexString): void; - - public decrypt(ctext: HexString): string; - - public decryptOAEP(ctext: HexString, hash?: string | ((s: string) => string), hashLen?: number): string | null; - - //// RSA PEM - - public getPosArrayOfChildrenFromHex(hPrivateKey: PEM): Idx[]; - - public getHexValueArrayOfChildrenFromHex(hPrivateKey: PEM): Idx[]; - - public readPrivateKeyFromPEMString(keyPEM: PEM): void; - - public readPKCS5PrvKeyHex(h: HexString): void; - - public readPKCS8PrvKeyHex(h: HexString): void; - - public readPKCS5PubKeyHex(h: HexString): void; - - public readPKCS8PubKeyHex(h: HexString): void; - - public readCertPubKeyHex(h: HexString, nthPKI: Nth): void; - - //// RSA SIGN - - public sign(s: string, hashAlg: string): HexString; - - public signWithMessageHash(sHashHex: HexString, hashAlg: string): HexString; - - public signPSS(s: string, hashAlg: string, sLen: number): HexString; - - public signWithMessageHashPSS(hHash: HexString, hashAlg: string, sLen: number): HexString; - - public verify(sMsg: string, hSig: HexString): boolean | 0; - - public verifyWithMessageHash(sHashHex: HexString, hSig: HexString): boolean | 0; - - public verifyPSS(sMsg: string, hSig: HexString, hashAlg: string, sLen: number): boolean; - - public verifyWithMessageHashPSS(hHash: HexString, hSig: HexString, hashAlg: string, sLen: number): boolean; - - public static SALT_LEN_HLEN: -1; - - public static SALT_LEN_MAX: -2; - - public static SALT_LEN_RECOVER: -2; - } - - /// RNG TYPES - class SecureRandom { - public nextBytes(ba: Mutable): void; - } - - //// X509 TYPES - - type ExtInfo = { - critical: boolean; - oid: OID; - vidx: Idx; - }; - - type ExtAIAInfo = Record<'ocsp' | 'caissuer', string>; - - type ExtCertificatePolicy = { - id: OIDName; - } & Partial<{ - cps: string; - } | { - unotice: string; - }>; - - class X509 { - public hex: HexString | null; - - public version: number; - - public foffset: number; - - public aExtInfo: null; - - public getVersion(): number; - - public getSerialNumberHex(): ASN1V; - - public getSignatureAlgorithmField(): OIDName; - - public getIssuerHex(): ASN1TLV; - - public getIssuerString(): HexString; - - public getSubjectHex(): ASN1TLV; - - public getSubjectString(): HexString; - - public getNotBefore(): TimeValue; - - public getNotAfter(): TimeValue; - - public getPublicKeyHex(): ASN1TLV; - - public getPublicKeyIdx(): Idx>; - - public getPublicKeyContentIdx(): Idx>; - - public getPublicKey(): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public getSignatureAlgorithmName(): OIDName; - - public getSignatureValueHex(): ASN1V; - - public verifySignature(pubKey: GetKeyParam): boolean | 0; - - public parseExt(): void; - - public getExtInfo(oidOrName: OID | string): ExtInfo | undefined; - - public getExtBasicConstraints(): ExtInfo | {} | { - cA: true; - pathLen?: number; - }; - - public getExtKeyUsageBin(): BinString; - - public getExtKeyUsageString(): string; - - public getExtSubjectKeyIdentifier(): ASN1V | undefined; - - public getExtAuthorityKeyIdentifier(): { - kid: ASN1V; - } | undefined; - - public getExtExtKeyUsageName(): OIDName[] | undefined; - - public getExtSubjectAltName(): Deprecated; - - public getExtSubjectAltName2(): ['MAIL' | 'DNS' | 'DN' | 'URI' | 'IP', string][] | undefined; - - public getExtCRLDistributionPointsURI(): string[] | undefined; - - public getExtAIAInfo(): ExtAIAInfo | undefined; - - public getExtCertificatePolicies(): ExtCertificatePolicy[] | undefined; - - public readCertPEM(sCertPEM: PEM): void; - - public readCertHex(sCertHex: HexString): void; - - public getInfo(): string; - - public static hex2dn(hex: HexString, idx?: Idx): string; - - public static hex2rdn(hex: HexString, idx?: Idx): string; - - public static hex2attrTypeValue(hex: HexString, idx?: Idx): string; - - public static getPublicKeyFromCertPEM(sCertPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public static getPublicKeyInfoPropOfCertPEM(sCertPEM: PEM): { - algparam: ASN1V | null; - leyhex: ASN1V; - algoid: ASN1V; - }; - } -} diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index 5bb20a729f..c3d0592256 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -1,6 +1,6 @@ import cluster from 'node:cluster'; import chalk from 'chalk'; -import { default as Xev } from 'xev'; +import Xev from 'xev'; import Logger from '@/services/logger.js'; import { envOption } from '../env.js'; @@ -12,7 +12,7 @@ import { workerMain } from './worker.js'; const logger = new Logger('core', 'cyan'); const clusterLogger = logger.createSubLogger('cluster', 'orange', false); -const ev = new Xev.default(); +const ev = new Xev(); /** * Init process diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 09d20f9361..bf51960482 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -5,7 +5,6 @@ import * as os from 'node:os'; import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; -import * as portscanner from 'portscanner'; import semver from 'semver'; import Logger from '@/services/logger.js'; @@ -48,11 +47,6 @@ function greet() { bootLogger.info(`Misskey v${meta.version}`, null, true); } -function isRoot() { - // maybe process.getuid will be undefined under not POSIX environment (e.g. Windows) - return process.getuid != null && process.getuid() === 0; -} - /** * Init master process */ @@ -67,7 +61,6 @@ export async function masterMain() { showNodejsVersion(); config = loadConfigBoot(); await connectDb(); - await validatePort(config); } catch (e) { bootLogger.error('Fatal error occurred during initialization', null, true); process.exit(1); @@ -97,8 +90,6 @@ function showEnvironment(): void { logger.warn('The environment is not in production mode.'); logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); } - - logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`); } function showNodejsVersion(): void { @@ -152,29 +143,6 @@ async function connectDb(): Promise { } } -async function validatePort(config: Config): Promise { - const isWellKnownPort = (port: number) => port < 1024; - - async function isPortAvailable(port: number): Promise { - return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed'; - } - - if (config.port == null || Number.isNaN(config.port)) { - bootLogger.error('The port is not configured. Please configure port.', null, true); - process.exit(1); - } - - if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) { - bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true); - process.exit(1); - } - - if (!await isPortAvailable(config.port)) { - bootLogger.error(`Port ${config.port} is already in use`, null, true); - process.exit(1); - } -} - async function spawnWorkers(limit: number = 1) { const workers = Math.min(limit, os.cpus().length); bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); @@ -186,6 +154,10 @@ function spawnWorker(): Promise { return new Promise(res => { const worker = cluster.fork(); worker.on('message', message => { + if (message === 'listenFailed') { + bootLogger.error(`The server Listen failed due to the previous error.`); + process.exit(1); + } if (message !== 'ready') return; res(); }); diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index 7f765463e4..9654a4f3b0 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -25,6 +25,7 @@ const path = process.env.NODE_ENV === 'test' export default function load() { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); + const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const mixin = {} as Mixin; @@ -45,6 +46,7 @@ export default function load() { mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`; + mixin.clientEntry = clientManifest['src/init.ts']; if (!config.redis.prefix) config.redis.prefix = mixin.host; diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 58a27803cb..948545db7a 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -80,6 +80,7 @@ export type Mixin = { authUrl: string; driveUrl: string; userAgent: string; + clientEntry: string; }; export type Config = Source & Mixin; diff --git a/packages/backend/src/daemons/queue-stats.ts b/packages/backend/src/daemons/queue-stats.ts index bfef110545..1535abc6af 100644 --- a/packages/backend/src/daemons/queue-stats.ts +++ b/packages/backend/src/daemons/queue-stats.ts @@ -1,7 +1,7 @@ -import { default as Xev } from 'xev'; +import Xev from 'xev'; import { deliverQueue, inboxQueue } from '../queue/queues.js'; -const ev = new Xev.default(); +const ev = new Xev(); const interval = 10000; diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts index 327305ccc5..faf4e6e4a4 100644 --- a/packages/backend/src/daemons/server-stats.ts +++ b/packages/backend/src/daemons/server-stats.ts @@ -1,8 +1,8 @@ import si from 'systeminformation'; -import { default as Xev } from 'xev'; +import Xev from 'xev'; import * as osUtils from 'os-utils'; -const ev = new Xev.default(); +const ev = new Xev(); const interval = 2000; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index eb5fc2e186..298f6713ea 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -5,9 +5,6 @@ pg.types.setTypeParser(20, Number); import { Logger, DataSource } from 'typeorm'; import * as highlight from 'cli-highlight'; import config from '@/config/index.js'; -import { envOption } from '../env.js'; - -import { dbLogger } from './logger.js'; import { User } from '@/models/entities/user.js'; import { DriveFile } from '@/models/entities/drive-file.js'; @@ -74,6 +71,9 @@ import { UserPending } from '@/models/entities/user-pending.js'; import { entities as charts } from '@/services/chart/entities.js'; import { Webhook } from '@/models/entities/webhook.js'; +import { envOption } from '../env.js'; +import { dbLogger } from './logger.js'; +import { redisClient } from './redis.js'; const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); @@ -208,16 +208,25 @@ export const db = new DataSource({ migrations: ['../../migration/*.js'], }); -export async function initDb() { +export async function initDb(force = false) { + if (force) { + if (db.isInitialized) { + await db.destroy(); + } + await db.initialize(); + return; + } + if (db.isInitialized) { // nop } else { - await db.connect(); + await db.initialize(); } } export async function resetDb() { const reset = async () => { + await redisClient.FLUSHDB(); const tables = await db.query(`SELECT relname AS "table" FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE nspname NOT IN ('pg_catalog', 'information_schema') diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts index 623cb0e71c..15110b6b70 100644 --- a/packages/backend/src/mfm/from-html.ts +++ b/packages/backend/src/mfm/from-html.ts @@ -6,6 +6,9 @@ const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; export function fromHtml(html: string, hashtagNames?: string[]): string { + // some AP servers like Pixelfed use br tags as well as newlines + html = html.replace(/\r?\n/gi, '\n'); + const dom = parse5.parseFragment(html); let text = ''; diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 01bbe98a85..e5b911ed32 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -48,6 +48,7 @@ export class Cache { // Cache MISS const value = await fetcher(); + this.set(key, value); return value; } diff --git a/packages/backend/src/misc/cafy-id.ts b/packages/backend/src/misc/cafy-id.ts deleted file mode 100644 index dd81c5c4cf..0000000000 --- a/packages/backend/src/misc/cafy-id.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Context } from 'cafy'; - -// eslint-disable-next-line @typescript-eslint/ban-types -export class ID extends Context { - public readonly name = 'ID'; - - constructor(optional = false, nullable = false) { - super(optional, nullable); - - this.push((v: any) => { - if (typeof v !== 'string') { - return new Error('must-be-an-id'); - } - return true; - }); - } - - public getType() { - return super.getType('String'); - } - - public makeOptional(): ID { - return new ID(true, false); - } - - public makeNullable(): ID { - return new ID(false, true); - } - - public makeOptionalNullable(): ID { - return new ID(true, true); - } -} diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index 04604cf7d0..f07be634fb 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -1,10 +1,19 @@ import * as tmp from 'tmp'; -export function createTemp(): Promise<[string, any]> { - return new Promise<[string, any]>((res, rej) => { +export function createTemp(): Promise<[string, () => void]> { + return new Promise<[string, () => void]>((res, rej) => { tmp.file((e, path, fd, cleanup) => { if (e) return rej(e); res([path, cleanup]); }); }); } + +export function createTempDir(): Promise<[string, () => void]> { + return new Promise<[string, () => void]>((res, rej) => { + tmp.dir((e, path, cleanup) => { + if (e) return rej(e); + res([path, cleanup]); + }); + }); +} diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts index 5417c10962..e855ac28ee 100644 --- a/packages/backend/src/misc/fetch-meta.ts +++ b/packages/backend/src/misc/fetch-meta.ts @@ -20,9 +20,16 @@ export async function fetchMeta(noCache = false): Promise { cache = meta; return meta; } else { - const saved = await transactionalEntityManager.save(Meta, { - id: 'x', - }) as Meta; + // metaใŒ็ฉบใฎใจใfetchMetaใŒๅŒๆ™‚ใซๅ‘ผใฐใ‚Œใ‚‹ใจใ“ใ“ใŒๅŒๆ™‚ใซๅ‘ผใฐใ‚Œใฆใ—ใพใ†ใ“ใจใŒใ‚ใ‚‹ใฎใงใƒ•ใ‚งใ‚คใƒซใ‚ปใƒผใƒ•ใชupsertใ‚’ไฝฟใ† + const saved = await transactionalEntityManager + .upsert( + Meta, + { + id: 'x', + }, + ['id'], + ) + .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); cache = saved; return saved; diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts index 4b1013c9f5..af6bf2fca7 100644 --- a/packages/backend/src/misc/fetch.ts +++ b/packages/backend/src/misc/fetch.ts @@ -1,10 +1,10 @@ -import * as http from 'http'; -import * as https from 'https'; +import * as http from 'node:http'; +import * as https from 'node:https'; +import { URL } from 'node:url'; import CacheableLookup from 'cacheable-lookup'; import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import config from '@/config/index.js'; -import { URL } from 'node:url'; export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record) { const res = await getResponse({ @@ -35,7 +35,7 @@ export async function getHtml(url: string, accept = 'text/html, */*', timeout = } export async function getResponse(args: { url: string, method: string, body?: string, headers: Record, timeout?: number, size?: number }) { - const timeout = args?.timeout || 10 * 1000; + const timeout = args.timeout || 10 * 1000; const controller = new AbortController(); setTimeout(() => { @@ -47,7 +47,7 @@ export async function getResponse(args: { url: string, method: string, body?: st headers: args.headers, body: args.body, timeout, - size: args?.size || 10 * 1024 * 1024, + size: args.size || 10 * 1024 * 1024, agent: getAgentByUrl, signal: controller.signal, }); diff --git a/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts new file mode 100644 index 0000000000..379325bb13 --- /dev/null +++ b/packages/backend/src/misc/get-ip-hash.ts @@ -0,0 +1,9 @@ +import IPCIDR from 'ip-cidr'; + +export function getIpHash(ip: string) { + // because a single person may control many IPv6 addresses, + // only a /64 subnet prefix of any IP will be taken into account. + // (this means for IPv4 the entire address is used) + const prefix = IPCIDR.createAddress(ip).mask(64); + return 'ip-' + BigInt('0b' + prefix).toString(36); +} diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts index 86f1356c31..6a185d09f6 100644 --- a/packages/backend/src/misc/populate-emojis.ts +++ b/packages/backend/src/misc/populate-emojis.ts @@ -63,7 +63,7 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu const isLocal = emoji.host == null; const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl ใ—ใฆใ‚‹ใฎใฏๅพŒๆ–นไบ’ๆ›ๆ€งใฎใŸใ‚ - const url = isLocal ? emojiUrl : `${config.url}/proxy/image.png?${query({ url: emojiUrl })}`; + const url = isLocal ? emojiUrl : `${config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`; return { name: emojiName, diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 5b69812090..fdecc278d4 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -89,7 +89,7 @@ export interface Schema extends OfSchema { readonly optional?: boolean; readonly items?: Schema; readonly properties?: Obj; - readonly required?: ReadonlyArray>; + readonly required?: ReadonlyArray, string>>; readonly description?: string; readonly example?: any; readonly format?: string; @@ -98,6 +98,9 @@ export interface Schema extends OfSchema { readonly default?: (this['type'] extends TypeStringef ? StringDefToType : any) | null; readonly maxLength?: number; readonly minLength?: number; + readonly maximum?: number; + readonly minimum?: number; + readonly pattern?: string; } type RequiredPropertyNames = { @@ -105,24 +108,26 @@ type RequiredPropertyNames = { // K is not optional s[K]['optional'] extends false ? K : // K has default value - s[K]['default'] extends null | string | number | boolean | Record ? K : never + s[K]['default'] extends null | string | number | boolean | Record ? K : + never }[keyof s]; -export interface Obj { [key: string]: Schema; } +export type Obj = Record; +// https://github.com/misskey-dev/misskey/issues/8535 +// To avoid excessive stack depth error, +// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). export type ObjType = - { -readonly [P in keyof s]?: SchemaType } & - { -readonly [P in RequiredProps]: SchemaType } & - { -readonly [P in RequiredPropertyNames]: SchemaType }; + UnionToIntersection< + { -readonly [R in RequiredPropertyNames]-?: SchemaType } & + { -readonly [R in RequiredProps]-?: SchemaType } & + { -readonly [P in keyof s]?: SchemaType } + >; type NullOrUndefined

= - p['nullable'] extends true - ? p['optional'] extends true - ? (T | null | undefined) - : (T | null) - : p['optional'] extends true - ? (T | undefined) - : T; + | (p['nullable'] extends true ? null : never) + | (p['optional'] extends true ? undefined : never) + | T; // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // Get intersection from union @@ -139,9 +144,9 @@ export type SchemaTypeDef

= p['type'] extends 'number' ? number : p['type'] extends 'string' ? ( p['enum'] extends readonly string[] ? - p['enum'][number] : - p['format'] extends 'date-time' ? string : // Dateใซใ™ใ‚‹๏ผŸ๏ผŸ - string + p['enum'][number] : + p['format'] extends 'date-time' ? string : // Dateใซใ™ใ‚‹๏ผŸ๏ผŸ + string ) : p['type'] extends 'boolean' ? boolean : p['type'] extends 'object' ? ( diff --git a/packages/backend/src/models/entities/access-token.ts b/packages/backend/src/models/entities/access-token.ts index 69cdc49cec..c6e2141a46 100644 --- a/packages/backend/src/models/entities/access-token.ts +++ b/packages/backend/src/models/entities/access-token.ts @@ -15,7 +15,6 @@ export class AccessToken { @Column('timestamp with time zone', { nullable: true, - default: null, }) public lastUsedAt: Date | null; @@ -29,7 +28,6 @@ export class AccessToken { @Column('varchar', { length: 128, nullable: true, - default: null, }) public session: string | null; @@ -52,7 +50,6 @@ export class AccessToken { @Column({ ...id(), nullable: true, - default: null, }) public appId: App['id'] | null; @@ -65,21 +62,18 @@ export class AccessToken { @Column('varchar', { length: 128, nullable: true, - default: null, }) public name: string | null; @Column('varchar', { length: 512, nullable: true, - default: null, }) public description: string | null; @Column('varchar', { length: 512, nullable: true, - default: null, }) public iconUrl: string | null; diff --git a/packages/backend/src/models/entities/auth-session.ts b/packages/backend/src/models/entities/auth-session.ts index b825856201..295d1b486c 100644 --- a/packages/backend/src/models/entities/auth-session.ts +++ b/packages/backend/src/models/entities/auth-session.ts @@ -23,7 +23,7 @@ export class AuthSession { ...id(), nullable: true, }) - public userId: User['id']; + public userId: User['id'] | null; @ManyToOne(type => User, { onDelete: 'CASCADE', diff --git a/packages/backend/src/models/entities/clip.ts b/packages/backend/src/models/entities/clip.ts index da6b3c7a7f..1386684c32 100644 --- a/packages/backend/src/models/entities/clip.ts +++ b/packages/backend/src/models/entities/clip.ts @@ -37,7 +37,7 @@ export class Clip { public isPublic: boolean; @Column('varchar', { - length: 2048, nullable: true, default: null, + length: 2048, nullable: true, comment: 'The description of the Clip.', }) public description: string | null; diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 3d375f0e35..a636d1d519 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -79,7 +79,6 @@ export class DriveFile { }) public properties: { width?: number; height?: number; orientation?: number; avgColor?: string }; - @Index() @Column('boolean') public storedInternal: boolean; diff --git a/packages/backend/src/models/entities/emoji.ts b/packages/backend/src/models/entities/emoji.ts index b72ca72331..7332dd1857 100644 --- a/packages/backend/src/models/entities/emoji.ts +++ b/packages/backend/src/models/entities/emoji.ts @@ -36,6 +36,7 @@ export class Emoji { @Column('varchar', { length: 512, + default: '', }) public publicUrl: string; diff --git a/packages/backend/src/models/entities/instance.ts b/packages/backend/src/models/entities/instance.ts index bb24d6b30f..7ea9234384 100644 --- a/packages/backend/src/models/entities/instance.ts +++ b/packages/backend/src/models/entities/instance.ts @@ -107,53 +107,53 @@ export class Instance { public isSuspended: boolean; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, comment: 'The software of the Instance.', }) public softwareName: string | null; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, }) public softwareVersion: string | null; @Column('boolean', { - nullable: true, default: null, + nullable: true, }) public openRegistrations: boolean | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public name: string | null; @Column('varchar', { - length: 4096, nullable: true, default: null, + length: 4096, nullable: true, }) public description: string | null; @Column('varchar', { - length: 128, nullable: true, default: null, + length: 128, nullable: true, }) public maintainerName: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public maintainerEmail: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public iconUrl: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public faviconUrl: string | null; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, }) public themeColor: string | null; diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 4d58b5f04f..80b5228bcd 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -78,7 +78,7 @@ export class Meta { public blockedHosts: string[]; @Column('varchar', { - length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}', + length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}', }) public pinnedPages: string[]; @@ -346,14 +346,12 @@ export class Meta { @Column('varchar', { length: 8192, - default: null, nullable: true, }) public defaultLightTheme: string | null; @Column('varchar', { length: 8192, - default: null, nullable: true, }) public defaultDarkTheme: string | null; diff --git a/packages/backend/src/models/entities/muting.ts b/packages/backend/src/models/entities/muting.ts index b3a7e7a671..8f9e69063a 100644 --- a/packages/backend/src/models/entities/muting.ts +++ b/packages/backend/src/models/entities/muting.ts @@ -17,7 +17,6 @@ export class Muting { @Index() @Column('timestamp with time zone', { nullable: true, - default: null, }) public expiresAt: Date | null; diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index da49d53b69..0ffeb85f69 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -53,8 +53,8 @@ export class Note { }) public threadId: string | null; - @Column('varchar', { - length: 8192, nullable: true, + @Column('text', { + nullable: true, }) public text: string | null; @@ -179,7 +179,7 @@ export class Note { @Index() @Column({ ...id(), - nullable: true, default: null, + nullable: true, comment: 'The ID of source channel.', }) public channelId: Channel['id'] | null; diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index f95cb144c5..1778742ea2 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -192,6 +192,7 @@ export class UserProfile { @Column('jsonb', { default: [], + comment: 'List of instances muted by the user.', }) public mutedInstances: string[]; diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index 29d9a0c2ca..df92fb8259 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -207,7 +207,7 @@ export class User { @Column('boolean', { default: false, - comment: 'Whether to show users replying to other users in the timeline', + comment: 'Whether to show users replying to other users in the timeline.', }) public showTimelineReplies: boolean; diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 69dc1721c2..0d589d4f11 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -1,6 +1,5 @@ import { db } from '@/db/postgre.js'; import { DriveFile } from '@/models/entities/drive-file.js'; -import { Users, DriveFolders } from '../index.js'; import { User } from '@/models/entities/user.js'; import { toPuny } from '@/misc/convert-host.js'; import { awaitAll, Promiseable } from '@/prelude/await-all.js'; @@ -9,6 +8,7 @@ import config from '@/config/index.js'; import { query, appendQuery } from '@/prelude/url.js'; import { Meta } from '@/models/entities/meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Users, DriveFolders } from '../index.js'; type PackOptions = { detail?: boolean, @@ -29,6 +29,8 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ getPublicProperties(file: DriveFile): DriveFile['properties'] { if (file.properties.orientation != null) { + // TODO + //const properties = structuredClone(file.properties); const properties = JSON.parse(JSON.stringify(file.properties)); if (file.properties.orientation >= 5) { [properties.width, properties.height] = [properties.height, properties.width]; @@ -111,7 +113,40 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ async pack( src: DriveFile['id'] | DriveFile, - options?: PackOptions + options?: PackOptions, + ): Promise> { + const opts = Object.assign({ + detail: false, + self: false, + }, options); + + const file = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); + + return await awaitAll>({ + id: file.id, + createdAt: file.createdAt.toISOString(), + name: file.name, + type: file.type, + md5: file.md5, + size: file.size, + isSensitive: file.isSensitive, + blurhash: file.blurhash, + properties: opts.self ? file.properties : this.getPublicProperties(file), + url: opts.self ? file.url : this.getPublicUrl(file, false), + thumbnailUrl: this.getPublicUrl(file, true), + comment: file.comment, + folderId: file.folderId, + folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { + detail: true, + }) : null, + userId: opts.withUser ? file.userId : null, + user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null, + }); + }, + + async packNullable( + src: DriveFile['id'] | DriveFile, + options?: PackOptions, ): Promise | null> { const opts = Object.assign({ detail: false, @@ -145,9 +180,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ async packMany( files: (DriveFile['id'] | DriveFile)[], - options?: PackOptions - ) { - const items = await Promise.all(files.map(f => this.pack(f, options))); - return items.filter(x => x != null); + options?: PackOptions, + ): Promise[]> { + const items = await Promise.all(files.map(f => this.packNullable(f, options))); + return items.filter((x): x is Packed<'DriveFile'> => x != null); }, }); diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index cf5fcb1787..3fefab0319 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -136,6 +136,7 @@ async function populateMyReaction(note: Note, meId: User['id'], _hint_?: { export const NoteRepository = db.getRepository(Note).extend({ async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { + // This code must always be synchronized with the checks in generateVisibilityQuery. // visibility ใŒ specified ใ‹ใค่‡ชๅˆ†ใŒๆŒ‡ๅฎšใ•ใ‚Œใฆใ„ใชใ‹ใฃใŸใ‚‰้ž่กจ็คบ if (note.visibility === 'specified') { if (meId == null) { @@ -144,13 +145,7 @@ export const NoteRepository = db.getRepository(Note).extend({ return true; } else { // ๆŒ‡ๅฎšใ•ใ‚Œใฆใ„ใ‚‹ใ‹ใฉใ†ใ‹ - const specified = note.visibleUserIds.some((id: any) => meId === id); - - if (specified) { - return true; - } else { - return false; - } + return note.visibleUserIds.some((id: any) => meId === id); } } @@ -168,16 +163,25 @@ export const NoteRepository = db.getRepository(Note).extend({ return true; } else { // ใƒ•ใ‚ฉใƒญใƒฏใƒผใ‹ใฉใ†ใ‹ - const following = await Followings.findOneBy({ - followeeId: note.userId, - followerId: meId, - }); + const [following, user] = await Promise.all([ + Followings.count({ + where: { + followeeId: note.userId, + followerId: meId, + }, + take: 1, + }), + Users.findOneByOrFail({ id: meId }), + ]); - if (following == null) { - return false; - } else { - return true; - } + /* If we know the following, everyhting is fine. + + But if we do not know the following, it might be that both the + author of the note and the author of the like are remote users, + in which case we can never know the following. Instead we have + to assume that the users are following each other. + */ + return following > 0 || (note.userHost != null && user.host != null); } } diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts index 1bffb23fa2..092b26b396 100644 --- a/packages/backend/src/models/repositories/page.ts +++ b/packages/backend/src/models/repositories/page.ts @@ -1,10 +1,10 @@ import { db } from '@/db/postgre.js'; import { Page } from '@/models/entities/page.js'; import { Packed } from '@/misc/schema.js'; -import { Users, DriveFiles, PageLikes } from '../index.js'; import { awaitAll } from '@/prelude/await-all.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { User } from '@/models/entities/user.js'; +import { Users, DriveFiles, PageLikes } from '../index.js'; export const PageRepository = db.getRepository(Page).extend({ async pack( @@ -14,7 +14,7 @@ export const PageRepository = db.getRepository(Page).extend({ const meId = me ? me.id : null; const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - const attachedFiles: Promise[] = []; + const attachedFiles: Promise[] = []; const collectFile = (xs: any[]) => { for (const x of xs) { if (x.type === 'image') { @@ -73,7 +73,7 @@ export const PageRepository = db.getRepository(Page).extend({ script: page.script, eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null, - attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)), + attachedFiles: DriveFiles.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)), likedCount: page.likedCount, isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, }); diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 541fbaf003..8a4e48efdd 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -61,47 +61,58 @@ export const UserRepository = db.getRepository(User).extend({ //#endregion async getRelation(me: User['id'], target: User['id']) { - const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ - Followings.findOneBy({ - followerId: me, - followeeId: target, - }), - Followings.findOneBy({ - followerId: target, - followeeId: me, - }), - FollowRequests.findOneBy({ - followerId: me, - followeeId: target, - }), - FollowRequests.findOneBy({ - followerId: target, - followeeId: me, - }), - Blockings.findOneBy({ - blockerId: me, - blockeeId: target, - }), - Blockings.findOneBy({ - blockerId: target, - blockeeId: me, - }), - Mutings.findOneBy({ - muterId: me, - muteeId: target, - }), - ]); - - return { + return awaitAll({ id: target, - isFollowing: following1 != null, - hasPendingFollowRequestFromYou: followReq1 != null, - hasPendingFollowRequestToYou: followReq2 != null, - isFollowed: following2 != null, - isBlocking: toBlocking != null, - isBlocked: fromBlocked != null, - isMuted: mute != null, - }; + isFollowing: Followings.count({ + where: { + followerId: me, + followeeId: target, + }, + take: 1, + }).then(n => n > 0), + isFollowed: Followings.count({ + where: { + followerId: target, + followeeId: me, + }, + take: 1, + }).then(n => n > 0), + hasPendingFollowRequestFromYou: FollowRequests.count({ + where: { + followerId: me, + followeeId: target, + }, + take: 1, + }).then(n => n > 0), + hasPendingFollowRequestToYou: FollowRequests.count({ + where: { + followerId: target, + followeeId: me, + }, + take: 1, + }).then(n => n > 0), + isBlocking: Blockings.count({ + where: { + blockerId: me, + blockeeId: target, + }, + take: 1, + }).then(n => n > 0), + isBlocked: Blockings.count({ + where: { + blockerId: target, + blockeeId: me, + }, + take: 1, + }).then(n => n > 0), + isMuted: Mutings.count({ + where: { + muterId: me, + muteeId: target, + }, + take: 1, + }).then(n => n > 0), + }); }, async getHasUnreadMessagingMessage(userId: User['id']): Promise { diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 2d40290e4c..c5fd7de1cb 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -1,4 +1,4 @@ -import httpSignature from 'http-signature'; +import httpSignature from '@peertube/http-signature'; import { v4 as uuid } from 'uuid'; import config from '@/config/index.js'; @@ -305,11 +305,13 @@ export default function() { systemQueue.add('resyncCharts', { }, { repeat: { cron: '0 0 * * *' }, + removeOnComplete: true, }); systemQueue.add('cleanCharts', { }, { repeat: { cron: '0 0 * * *' }, + removeOnComplete: true, }); systemQueue.add('checkExpiredMutings', { diff --git a/packages/backend/src/queue/processors/db/export-blocking.ts b/packages/backend/src/queue/processors/db/export-blocking.ts index 166c9e4cd3..f5e0424a79 100644 --- a/packages/backend/src/queue/processors/db/export-blocking.ts +++ b/packages/backend/src/queue/processors/db/export-blocking.ts @@ -1,11 +1,11 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; +import { createTemp } from '@/misc/create-temp.js'; import { Users, Blockings } from '@/models/index.js'; import { MoreThan } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; @@ -22,73 +22,72 @@ export async function exportBlocking(job: Bull.Job, done: any): P } // Create temp file - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - let exportedCount = 0; - let cursor: any = null; + let exportedCount = 0; + let cursor: any = null; - while (true) { - const blockings = await Blockings.find({ - where: { - blockerId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); + while (true) { + const blockings = await Blockings.find({ + where: { + blockerId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); - if (blockings.length === 0) { - job.progress(100); - break; - } - - cursor = blockings[blockings.length - 1].id; - - for (const block of blockings) { - const u = await Users.findOneBy({ id: block.blockeeId }); - if (u == null) { - exportedCount++; continue; + if (blockings.length === 0) { + job.progress(100); + break; } - const content = getFullApAccount(u.username, u.host); - await new Promise((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + cursor = blockings[blockings.length - 1].id; + + for (const block of blockings) { + const u = await Users.findOneBy({ id: block.blockeeId }); + if (u == null) { + exportedCount++; continue; + } + + const content = getFullApAccount(u.username, u.host); + await new Promise((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); + exportedCount++; + } + + const total = await Blockings.countBy({ + blockerId: user.id, }); - exportedCount++; + + job.progress(exportedCount / total); } - const total = await Blockings.countBy({ - blockerId: user.id, - }); + stream.end(); + logger.succ(`Exported to: ${path}`); - job.progress(exportedCount / total); + const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); + + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); } - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts index c2467fb5f0..8ce1d05272 100644 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/export-custom-emojis.ts @@ -1,5 +1,4 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { ulid } from 'ulid'; @@ -10,6 +9,7 @@ import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { Users, Emojis } from '@/models/index.js'; import { } from '@/queue/types.js'; +import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import config from '@/config/index.js'; import { IsNull } from 'typeorm'; @@ -25,13 +25,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi return; } - // Create temp dir - const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { - tmp.dir((e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTempDir(); logger.info(`Temp dir is ${path}`); @@ -98,12 +92,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi metaStream.end(); // Create archive - const [archivePath, archiveCleanup] = await new Promise<[string, () => void]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [archivePath, archiveCleanup] = await createTemp(); const archiveStream = fs.createWriteStream(archivePath); const archive = archiver('zip', { zlib: { level: 0 }, diff --git a/packages/backend/src/queue/processors/db/export-following.ts b/packages/backend/src/queue/processors/db/export-following.ts index 965500ac27..4ac165567b 100644 --- a/packages/backend/src/queue/processors/db/export-following.ts +++ b/packages/backend/src/queue/processors/db/export-following.ts @@ -1,11 +1,11 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; +import { createTemp } from '@/misc/create-temp.js'; import { Users, Followings, Mutings } from '@/models/index.js'; import { In, MoreThan, Not } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; @@ -23,73 +23,72 @@ export async function exportFollowing(job: Bull.Job, done: () => } // Create temp file - const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - let cursor: Following['id'] | null = null; + let cursor: Following['id'] | null = null; - const mutings = job.data.excludeMuting ? await Mutings.findBy({ - muterId: user.id, - }) : []; + const mutings = job.data.excludeMuting ? await Mutings.findBy({ + muterId: user.id, + }) : []; - while (true) { - const followings = await Followings.find({ - where: { - followerId: user.id, - ...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}), - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as Following[]; + while (true) { + const followings = await Followings.find({ + where: { + followerId: user.id, + ...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}), + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as Following[]; - if (followings.length === 0) { - break; - } - - cursor = followings[followings.length - 1].id; - - for (const following of followings) { - const u = await Users.findOneBy({ id: following.followeeId }); - if (u == null) { - continue; + if (followings.length === 0) { + break; } - if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { - continue; - } + cursor = followings[followings.length - 1].id; - const content = getFullApAccount(u.username, u.host); - await new Promise((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + for (const following of followings) { + const u = await Users.findOneBy({ id: following.followeeId }); + if (u == null) { + continue; + } + + if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { + continue; + } + + const content = getFullApAccount(u.username, u.host); + await new Promise((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); - }); + } } + + stream.end(); + logger.succ(`Exported to: ${path}`); + + const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); + + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); } - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/export-mute.ts b/packages/backend/src/queue/processors/db/export-mute.ts index 0ef81971f1..6a36cfa072 100644 --- a/packages/backend/src/queue/processors/db/export-mute.ts +++ b/packages/backend/src/queue/processors/db/export-mute.ts @@ -1,11 +1,11 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; +import { createTemp } from '@/misc/create-temp.js'; import { Users, Mutings } from '@/models/index.js'; import { IsNull, MoreThan } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; @@ -22,74 +22,73 @@ export async function exportMute(job: Bull.Job, done: any): Promi } // Create temp file - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - let exportedCount = 0; - let cursor: any = null; + let exportedCount = 0; + let cursor: any = null; - while (true) { - const mutes = await Mutings.find({ - where: { - muterId: user.id, - expiresAt: IsNull(), - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); + while (true) { + const mutes = await Mutings.find({ + where: { + muterId: user.id, + expiresAt: IsNull(), + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }); - if (mutes.length === 0) { - job.progress(100); - break; - } - - cursor = mutes[mutes.length - 1].id; - - for (const mute of mutes) { - const u = await Users.findOneBy({ id: mute.muteeId }); - if (u == null) { - exportedCount++; continue; + if (mutes.length === 0) { + job.progress(100); + break; } - const content = getFullApAccount(u.username, u.host); - await new Promise((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + cursor = mutes[mutes.length - 1].id; + + for (const mute of mutes) { + const u = await Users.findOneBy({ id: mute.muteeId }); + if (u == null) { + exportedCount++; continue; + } + + const content = getFullApAccount(u.username, u.host); + await new Promise((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); + exportedCount++; + } + + const total = await Mutings.countBy({ + muterId: user.id, }); - exportedCount++; + + job.progress(exportedCount / total); } - const total = await Mutings.countBy({ - muterId: user.id, - }); + stream.end(); + logger.succ(`Exported to: ${path}`); - job.progress(exportedCount / total); + const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); + + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); } - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/export-notes.ts b/packages/backend/src/queue/processors/db/export-notes.ts index 7e12a6fac2..051fcdf385 100644 --- a/packages/backend/src/queue/processors/db/export-notes.ts +++ b/packages/backend/src/queue/processors/db/export-notes.ts @@ -1,5 +1,4 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; @@ -10,6 +9,7 @@ import { MoreThan } from 'typeorm'; import { Note } from '@/models/entities/note.js'; import { Poll } from '@/models/entities/poll.js'; import { DbUserJobData } from '@/queue/types.js'; +import { createTemp } from '@/misc/create-temp.js'; const logger = queueLogger.createSubLogger('export-notes'); @@ -23,82 +23,81 @@ export async function exportNotes(job: Bull.Job, done: any): Prom } // Create temp file - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - const write = (text: string): Promise => { - return new Promise((res, rej) => { - stream.write(text, err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } + const write = (text: string): Promise => { + return new Promise((res, rej) => { + stream.write(text, err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); }); - }); - }; + }; - await write('['); + await write('['); - let exportedNotesCount = 0; - let cursor: Note['id'] | null = null; + let exportedNotesCount = 0; + let cursor: Note['id'] | null = null; - while (true) { - const notes = await Notes.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as Note[]; + while (true) { + const notes = await Notes.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + }) as Note[]; - if (notes.length === 0) { - job.progress(100); - break; - } - - cursor = notes[notes.length - 1].id; - - for (const note of notes) { - let poll: Poll | undefined; - if (note.hasPoll) { - poll = await Polls.findOneByOrFail({ noteId: note.id }); + if (notes.length === 0) { + job.progress(100); + break; } - const content = JSON.stringify(serialize(note, poll)); - const isFirst = exportedNotesCount === 0; - await write(isFirst ? content : ',\n' + content); - exportedNotesCount++; + + cursor = notes[notes.length - 1].id; + + for (const note of notes) { + let poll: Poll | undefined; + if (note.hasPoll) { + poll = await Polls.findOneByOrFail({ noteId: note.id }); + } + const content = JSON.stringify(serialize(note, poll)); + const isFirst = exportedNotesCount === 0; + await write(isFirst ? content : ',\n' + content); + exportedNotesCount++; + } + + const total = await Notes.countBy({ + userId: user.id, + }); + + job.progress(exportedNotesCount / total); } - const total = await Notes.countBy({ - userId: user.id, - }); + await write(']'); - job.progress(exportedNotesCount / total); + stream.end(); + logger.succ(`Exported to: ${path}`); + + const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); + + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); } - await write(']'); - - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/export-user-lists.ts b/packages/backend/src/queue/processors/db/export-user-lists.ts index 45852a6038..71dd72df27 100644 --- a/packages/backend/src/queue/processors/db/export-user-lists.ts +++ b/packages/backend/src/queue/processors/db/export-user-lists.ts @@ -1,11 +1,11 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import { queueLogger } from '../../logger.js'; import { addFile } from '@/services/drive/add-file.js'; import { format as dateFormat } from 'date-fns'; import { getFullApAccount } from '@/misc/convert-host.js'; +import { createTemp } from '@/misc/create-temp.js'; import { Users, UserLists, UserListJoinings } from '@/models/index.js'; import { In } from 'typeorm'; import { DbUserJobData } from '@/queue/types.js'; @@ -26,46 +26,45 @@ export async function exportUserLists(job: Bull.Job, done: any): }); // Create temp file - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); logger.info(`Temp file is ${path}`); - const stream = fs.createWriteStream(path, { flags: 'a' }); + try { + const stream = fs.createWriteStream(path, { flags: 'a' }); - for (const list of lists) { - const joinings = await UserListJoinings.findBy({ userListId: list.id }); - const users = await Users.findBy({ - id: In(joinings.map(j => j.userId)), - }); - - for (const u of users) { - const acct = getFullApAccount(u.username, u.host); - const content = `${list.name},${acct}`; - await new Promise((res, rej) => { - stream.write(content + '\n', err => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); + for (const list of lists) { + const joinings = await UserListJoinings.findBy({ userListId: list.id }); + const users = await Users.findBy({ + id: In(joinings.map(j => j.userId)), }); + + for (const u of users) { + const acct = getFullApAccount(u.username, u.host); + const content = `${list.name},${acct}`; + await new Promise((res, rej) => { + stream.write(content + '\n', err => { + if (err) { + logger.error(err); + rej(err); + } else { + res(); + } + }); + }); + } } + + stream.end(); + logger.succ(`Exported to: ${path}`); + + const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; + const driveFile = await addFile({ user, path, name: fileName, force: true }); + + logger.succ(`Exported to: ${driveFile.id}`); + } finally { + cleanup(); } - stream.end(); - logger.succ(`Exported to: ${path}`); - - const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; - const driveFile = await addFile({ user, path, name: fileName, force: true }); - - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); done(); } diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts index 28e0b867a4..64dfe85374 100644 --- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/import-custom-emojis.ts @@ -1,9 +1,9 @@ import Bull from 'bull'; -import * as tmp from 'tmp'; import * as fs from 'node:fs'; import unzipper from 'unzipper'; import { queueLogger } from '../../logger.js'; +import { createTempDir } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { DriveFiles, Emojis } from '@/models/index.js'; import { DbUserImportJobData } from '@/queue/types.js'; @@ -25,13 +25,7 @@ export async function importCustomEmojis(job: Bull.Job, don return; } - // Create temp dir - const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { - tmp.dir((e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTempDir(); logger.info(`Temp dir is ${path}`); diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index 4fbfdb234f..198dde6050 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -1,6 +1,6 @@ import { URL } from 'node:url'; import Bull from 'bull'; -import httpSignature from 'http-signature'; +import httpSignature from '@peertube/http-signature'; import perform from '@/remote/activitypub/perform.js'; import Logger from '@/services/logger.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 6c0b9d9bf6..5ea4725561 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -3,7 +3,7 @@ import { Note } from '@/models/entities/note'; import { User } from '@/models/entities/user.js'; import { Webhook } from '@/models/entities/webhook'; import { IActivity } from '@/remote/activitypub/type.js'; -import httpSignature from 'http-signature'; +import httpSignature from '@peertube/http-signature'; export type DeliverJobData = { /** Actor */ diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index ef07966e42..1a02f675ca 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/ import { UserPublickey } from '@/models/entities/user-publickey.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; -import { IObject, getApId } from './type.js'; -import { resolvePerson } from './models/person.js'; import { Cache } from '@/misc/cache.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; +import { IObject, getApId } from './type.js'; +import { resolvePerson } from './models/person.js'; const publicKeyCache = new Cache(Infinity); const publicKeyByUserIdCache = new Cache(Infinity); +export type UriParseResult = { + /** wether the URI was generated by us */ + local: true; + /** id in DB */ + id: string; + /** hint of type, e.g. "notes", "users" */ + type: string; + /** any remaining text after type and id, not including the slash after id. undefined if empty */ + rest?: string; +} | { + /** wether the URI was generated by us */ + local: false; + /** uri in DB */ + uri: string; +}; + +export function parseUri(value: string | IObject): UriParseResult { + const uri = getApId(value); + + // the host part of a URL is case insensitive, so use the 'i' flag. + const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); + const matchLocal = uri.match(localRegex); + + if (matchLocal) { + return { + local: true, + type: matchLocal[1], + id: matchLocal[2], + rest: matchLocal[3], + }; + } else { + return { + local: false, + uri, + }; + } +} + export default class DbResolver { constructor() { } @@ -21,60 +59,54 @@ export default class DbResolver { * AP Note => Misskey Note in DB */ public async getNoteFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'notes') return null; - if (parsed.id) { return await Notes.findOneBy({ id: parsed.id, }); - } - - if (parsed.uri) { + } else { return await Notes.findOneBy({ uri: parsed.uri, }); } - - return null; } public async getMessageFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'notes') return null; - if (parsed.id) { return await MessagingMessages.findOneBy({ id: parsed.id, }); - } - - if (parsed.uri) { + } else { return await MessagingMessages.findOneBy({ uri: parsed.uri, }); } - - return null; } /** * AP Person => Misskey User in DB */ public async getUserFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); + const parsed = parseUri(value); + + if (parsed.local) { + if (parsed.type !== 'users') return null; - if (parsed.id) { return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({ id: parsed.id, }).then(x => x ?? undefined)) ?? null; - } - - if (parsed.uri) { + } else { return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ uri: parsed.uri, })); } - - return null; } /** @@ -120,31 +152,4 @@ export default class DbResolver { key, }; } - - public parseUri(value: string | IObject): UriParseResult { - const uri = getApId(value); - - const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)'); - const matchLocal = uri.match(localRegex); - - if (matchLocal) { - return { - type: matchLocal[1], - id: matchLocal[2], - }; - } else { - return { - uri, - }; - } - } } - -type UriParseResult = { - /** id in DB (local object only) */ - id?: string; - /** uri in DB (remote object only) */ - uri?: string; - /** hint of type (local object only, ex: notes, users) */ - type?: string -}; diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts index 680749f4d8..759cb4ae83 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts @@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; import { getApLock } from '@/misc/app-lock.js'; import { parseAudience } from '../../audience.js'; import { StatusError } from '@/misc/fetch.js'; +import { Notes } from '@/models/index.js'; const logger = apLogger; @@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac throw e; } + if (!await Notes.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity'; + logger.info(`Creating the (Re)Note: ${uri}`); const activityAudience = await parseAudience(actor, activity.to, activity.cc); diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts index 4c06a9de0b..c7064f553b 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/index.ts @@ -13,37 +13,37 @@ export default async (actor: CacheableRemoteUser, activity: IDelete): Promise x.href as string)); @@ -12,7 +12,7 @@ export async function extractApMentions(tags: IObject | IObject[] | null | undef const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( - hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))) + hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))), )).filter((x): x is CacheableUser => x != null); return mentionedUsers; diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 097a716614..56c1a483ad 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit'; import config from '@/config/index.js'; import Resolver from '../resolver.js'; import post from '@/services/note/create.js'; -import { resolvePerson, updatePerson } from './person.js'; +import { resolvePerson } from './person.js'; import { resolveImage } from './image.js'; -import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js'; +import { CacheableRemoteUser } from '@/models/entities/user.js'; import { htmlToMfm } from '../misc/html-to-mfm.js'; import { extractApHashtags } from './tag.js'; import { unique, toArray, toSingle } from '@/prelude/array.js'; @@ -15,7 +15,7 @@ import { apLogger } from '../logger.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { extractDbHost, toPuny } from '@/misc/convert-host.js'; -import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js'; +import { Emojis, Polls, MessagingMessages } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; import { Emoji } from '@/models/entities/emoji.js'; @@ -197,7 +197,14 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s const cw = note.summary === '' ? null : note.summary; // ใƒ†ใ‚ญใ‚นใƒˆใฎใƒ‘ใƒผใ‚น - const text = typeof note._misskey_content !== 'undefined' ? note._misskey_content : (note.content ? htmlToMfm(note.content, note.tag) : null); + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source?.content === 'string') { + text = note.source.content; + } else if (typeof note._misskey_content === 'string') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = htmlToMfm(note.content, note.tag); + } // vote if (reply && reply.hasPoll) { diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 88661865da..6097e3b6ed 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -1,17 +1,8 @@ import { URL } from 'node:url'; import promiseLimit from 'promise-limit'; -import $, { Context } from 'cafy'; import config from '@/config/index.js'; -import Resolver from '../resolver.js'; -import { resolveImage } from './image.js'; -import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js'; -import { fromHtml } from '../../../mfm/from-html.js'; -import { htmlToMfm } from '../misc/html-to-mfm.js'; -import { resolveNote, extractEmojis } from './note.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; -import { extractApHashtags } from './tag.js'; -import { apLogger } from '../logger.js'; import { Note } from '@/models/entities/note.js'; import { updateUsertags } from '@/services/update-hashtag.js'; import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; @@ -32,6 +23,14 @@ import { StatusError } from '@/misc/fetch.js'; import { uriPersonCache } from '@/services/user-cache.js'; import { publishInternalEvent } from '@/services/stream.js'; import { db } from '@/db/postgre.js'; +import { apLogger } from '../logger.js'; +import { htmlToMfm } from '../misc/html-to-mfm.js'; +import { fromHtml } from '../../../mfm/from-html.js'; +import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js'; +import Resolver from '../resolver.js'; +import { extractApHashtags } from './tag.js'; +import { resolveNote, extractEmojis } from './note.js'; +import { resolveImage } from './image.js'; const logger = apLogger; @@ -54,20 +53,33 @@ function validateActor(x: IObject, uri: string): IActor { throw new Error(`invalid Actor type '${x.type}'`); } - const validate = (name: string, value: any, validater: Context) => { - const e = validater.test(value); - if (e) throw new Error(`invalid Actor: ${name} ${e.message}`); - }; + if (!(typeof x.id === 'string' && x.id.length > 0)) { + throw new Error('invalid Actor: wrong id'); + } - validate('id', x.id, $.default.str.min(1)); - validate('inbox', x.inbox, $.default.str.min(1)); - validate('preferredUsername', x.preferredUsername, $.default.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/)); + if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { + throw new Error('invalid Actor: wrong inbox'); + } + + if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { + throw new Error('invalid Actor: wrong username'); + } // These fields are only informational, and some AP software allows these // fields to be very long. If they are too long, we cut them off. This way // we can at least see these users and their activities. - validate('name', truncate(x.name, nameLength), $.default.optional.nullable.str); - validate('summary', truncate(x.summary, summaryLength), $.default.optional.nullable.str); + if (x.name) { + if (!(typeof x.name === 'string' && x.name.length > 0)) { + throw new Error('invalid Actor: wrong name'); + } + x.name = truncate(x.name, nameLength); + } + if (x.summary) { + if (!(typeof x.summary === 'string' && x.summary.length > 0)) { + throw new Error('invalid Actor: wrong summary'); + } + x.summary = truncate(x.summary, summaryLength); + } const idHost = toPuny(new URL(x.id!).hostname); if (idHost !== expectHost) { @@ -271,7 +283,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise): Promise { +export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); // URIใŒใ“ใฎใ‚ตใƒผใƒใƒผใ‚’ๆŒ‡ใ—ใฆใ„ใ‚‹ใชใ‚‰ใ‚นใ‚ญใƒƒใƒ— @@ -289,7 +301,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint if (resolver == null) resolver = new Resolver(); - const object = hint || await resolver.resolve(uri) as any; + const object = hint || await resolver.resolve(uri); const person = validateActor(object, uri); @@ -400,10 +412,10 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise any } = { - 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }), - 'misskey:authentication:github': (id, login) => ({ id, login }), - 'misskey:authentication:discord': (id, name) => $discord(id, name), -}; + 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }), + 'misskey:authentication:github': (id, login) => ({ id, login }), + 'misskey:authentication:discord': (id, name) => $discord(id, name), + }; const $discord = (id: string, name: string) => { if (typeof name !== 'string') { @@ -461,7 +473,7 @@ export async function updateFeatured(userId: User['id']) { // Resolve to (Ordered)Collection Object const collection = await resolver.resolveCollection(user.featured); - if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`); + if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; diff --git a/packages/backend/src/remote/activitypub/renderer/block.ts b/packages/backend/src/remote/activitypub/renderer/block.ts index 10a4fde517..13815fb76f 100644 --- a/packages/backend/src/remote/activitypub/renderer/block.ts +++ b/packages/backend/src/remote/activitypub/renderer/block.ts @@ -1,8 +1,20 @@ import config from '@/config/index.js'; -import { ILocalUser, IRemoteUser } from '@/models/entities/user.js'; +import { Blocking } from '@/models/entities/blocking.js'; -export default (blocker: ILocalUser, blockee: IRemoteUser) => ({ - type: 'Block', - actor: `${config.url}/users/${blocker.id}`, - object: blockee.uri, -}); +/** + * Renders a block into its ActivityPub representation. + * + * @param block The block to be rendered. The blockee relation must be loaded. + */ +export function renderBlock(block: Blocking) { + if (block.blockee?.url == null) { + throw new Error('renderBlock: missing blockee uri'); + } + + return { + type: 'Block', + id: `${config.url}/blocks/${block.id}`, + actor: `${config.url}/users/${block.blockerId}`, + object: block.blockee.uri, + }; +} diff --git a/packages/backend/src/remote/activitypub/renderer/flag.ts b/packages/backend/src/remote/activitypub/renderer/flag.ts index 6fbc11580f..58eadddbaa 100644 --- a/packages/backend/src/remote/activitypub/renderer/flag.ts +++ b/packages/backend/src/remote/activitypub/renderer/flag.ts @@ -5,7 +5,7 @@ import { getInstanceActor } from '@/services/instance-actor.js'; // to anonymise reporters, the reporting actor must be a system user // object has to be a uri or array of uris -export const renderFlag = (user: ILocalUser, object: [string], content: string): IActivity => { +export const renderFlag = (user: ILocalUser, object: [string], content: string) => { return { type: 'Flag', actor: `${config.url}/users/${user.id}`, diff --git a/packages/backend/src/remote/activitypub/renderer/follow.ts b/packages/backend/src/remote/activitypub/renderer/follow.ts index 9e9692b77a..00fac18ad5 100644 --- a/packages/backend/src/remote/activitypub/renderer/follow.ts +++ b/packages/backend/src/remote/activitypub/renderer/follow.ts @@ -4,12 +4,11 @@ import { Users } from '@/models/index.js'; export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => { const follow = { + id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`, type: 'Follow', actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri, } as any; - if (requestId) follow.id = requestId; - return follow; }; diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts index 5f69332266..f100b77ce5 100644 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ b/packages/backend/src/remote/activitypub/renderer/index.ts @@ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js'; export const renderActivity = (x: any): IActivity | null => { if (x == null) return null; - if (x !== null && typeof x === 'object' && x.id == null) { + if (typeof x === 'object' && x.id == null) { x.id = `${config.url}/${uuid()}`; } diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 679c8bbfe4..df2ae65205 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,15 +1,15 @@ -import renderDocument from './document.js'; -import renderHashtag from './hashtag.js'; -import renderMention from './mention.js'; -import renderEmoji from './emoji.js'; +import { In, IsNull } from 'typeorm'; import config from '@/config/index.js'; -import toHtml from '../misc/get-note-html.js'; import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; -import { In, IsNull } from 'typeorm'; import { Emoji } from '@/models/entities/emoji.js'; import { Poll } from '@/models/entities/poll.js'; +import toHtml from '../misc/get-note-html.js'; +import renderEmoji from './emoji.js'; +import renderMention from './mention.js'; +import renderHashtag from './hashtag.js'; +import renderDocument from './document.js'; export default async function renderNote(note: Note, dive = true, isTalk = false): Promise> { const getPromisedFiles = async (ids: string[]) => { @@ -82,15 +82,15 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const files = await getPromisedFiles(note.fileIds); - const text = note.text; - let poll: Poll | null; + // text should never be undefined + const text = note.text ?? null; + let poll: Poll | null = null; if (note.hasPoll) { poll = await Polls.findOneBy({ noteId: note.id }); } - let apText = text; - if (apText == null) apText = ''; + let apText = text ?? ''; if (quote) { apText += `\n\nRE: ${quote}`; @@ -138,6 +138,10 @@ export default async function renderNote(note: Note, dive = true, isTalk = false summary, content, _misskey_content: text, + source: { + content: text, + mediaType: "text/x.misskeymarkdown", + }, _misskey_quote: quote, quoteUrl: quote, published: note.createdAt.toISOString(), @@ -159,7 +163,7 @@ export async function getEmojis(names: string[]): Promise { names.map(name => Emojis.findOneBy({ name, host: IsNull(), - })) + })), ); return emojis.filter(emoji => emoji != null) as Emoji[]; diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index c1269c75c5..2f9af43c0c 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -2,10 +2,19 @@ import config from '@/config/index.js'; import { getJson } from '@/misc/fetch.js'; import { ILocalUser } from '@/models/entities/user.js'; import { getInstanceActor } from '@/services/instance-actor.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; +import { extractDbHost, isSelfHost } from '@/misc/convert-host.js'; import { signedGet } from './request.js'; import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { extractDbHost } from '@/misc/convert-host.js'; +import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js'; +import { parseUri } from './db-resolver.js'; +import renderNote from '@/remote/activitypub/renderer/note.js'; +import { renderLike } from '@/remote/activitypub/renderer/like.js'; +import { renderPerson } from '@/remote/activitypub/renderer/person.js'; +import renderQuestion from '@/remote/activitypub/renderer/question.js'; +import renderCreate from '@/remote/activitypub/renderer/create.js'; +import { renderActivity } from '@/remote/activitypub/renderer/index.js'; +import renderFollow from '@/remote/activitypub/renderer/follow.js'; export default class Resolver { private history: Set; @@ -40,14 +49,25 @@ export default class Resolver { return value; } + if (value.includes('#')) { + // URLs with fragment parts cannot be resolved correctly because + // the fragment part does not get transmitted over HTTP(S). + // Avoid strange behaviour by not trying to resolve these at all. + throw new Error(`cannot resolve URL with fragment: ${value}`); + } + if (this.history.has(value)) { throw new Error('cannot resolve already resolved one'); } this.history.add(value); - const meta = await fetchMeta(); const host = extractDbHost(value); + if (isSelfHost(host)) { + return await this.resolveLocal(value); + } + + const meta = await fetchMeta(); if (meta.blockedHosts.includes(host)) { throw new Error('Instance is blocked'); } @@ -56,13 +76,13 @@ export default class Resolver { this.user = await getInstanceActor(); } - const object = this.user + const object = (this.user ? await signedGet(value, this.user) - : await getJson(value, 'application/activity+json, application/ld+json'); + : await getJson(value, 'application/activity+json, application/ld+json')) as IObject; if (object == null || ( Array.isArray(object['@context']) ? - !object['@context'].includes('https://www.w3.org/ns/activitystreams') : + !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' )) { throw new Error('invalid response'); @@ -70,4 +90,44 @@ export default class Resolver { return object; } + + private resolveLocal(url: string): Promise { + const parsed = parseUri(url); + if (!parsed.local) throw new Error('resolveLocal: not local'); + + switch (parsed.type) { + case 'notes': + return Notes.findOneByOrFail({ id: parsed.id }) + .then(note => { + if (parsed.rest === 'activity') { + // this refers to the create activity and not the note itself + return renderActivity(renderCreate(renderNote(note))); + } else { + return renderNote(note); + } + }); + case 'users': + return Users.findOneByOrFail({ id: parsed.id }) + .then(user => renderPerson(user as ILocalUser)); + case 'questions': + // Polls are indexed by the note they are attached to. + return Promise.all([ + Notes.findOneByOrFail({ id: parsed.id }), + Polls.findOneByOrFail({ noteId: parsed.id }), + ]) + .then(([note, poll]) => renderQuestion({ id: note.userId }, note, poll)); + case 'likes': + return NoteReactions.findOneByOrFail({ id: parsed.id }).then(reaction => renderActivity(renderLike(reaction, { uri: null }))); + case 'follows': + // rest should be + if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); + + return Promise.all( + [parsed.id, parsed.rest].map(id => Users.findOneByOrFail({ id })) + ) + .then(([follower, followee]) => renderActivity(renderFollow(follower, followee, url))); + default: + throw new Error(`resolveLocal: type ${type} unhandled`); + } + } } diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index 2051d2624d..5d00481b75 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -2,7 +2,7 @@ export type obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; export interface IObject { - '@context': string | obj | obj[]; + '@context': string | string[] | obj | obj[]; type: string | string[]; id?: string; summary?: string; @@ -48,7 +48,7 @@ export function getOneApId(value: ApObject): string { export function getApId(value: string | IObject): string { if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error(`cannot detemine id`); + throw new Error('cannot detemine id'); } /** @@ -57,7 +57,7 @@ export function getApId(value: string | IObject): string { export function getApType(value: IObject): string { if (typeof value.type === 'string') return value.type; if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; - throw new Error(`cannot detect type`); + throw new Error('cannot detect type'); } export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { @@ -106,7 +106,10 @@ export const isPost = (object: IObject): object is IPost => export interface IPost extends IObject { type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; - _misskey_content?: string; + source?: { + content: string; + mediaType: string; + }; _misskey_quote?: string; quoteUrl?: string; _misskey_talk: boolean; @@ -114,7 +117,10 @@ export interface IPost extends IObject { export interface IQuestion extends IObject { type: 'Note' | 'Question'; - _misskey_content?: string; + source?: { + content: string; + mediaType: string; + }; _misskey_quote?: string; quoteUrl?: string; oneOf?: IQuestionChoice[]; diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 133dd36066..cd5f917c40 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -1,6 +1,6 @@ import Router from '@koa/router'; import json from 'koa-json-body'; -import httpSignature from 'http-signature'; +import httpSignature from '@peertube/http-signature'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderNote from '@/remote/activitypub/renderer/note.js'; @@ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js'; import { isSelfHost } from '@/misc/convert-host.js'; import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js'; import { ILocalUser, User } from '@/models/entities/user.js'; -import { In, IsNull } from 'typeorm'; +import { In, IsNull, Not } from 'typeorm'; import { renderLike } from '@/remote/activitypub/renderer/like.js'; import { getUserKeypair } from '@/misc/keypair-store.js'; +import renderFollow from '@/remote/activitypub/renderer/follow.js'; // Init router const router = new Router(); @@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => { setResponseType(ctx); }); +// follow +router.get('/follows/:follower/:followee', async ctx => { + // This may be used before the follow is completed, so we do not + // check if the following exists. + + const [follower, followee] = await Promise.all([ + Users.findOneBy({ + id: ctx.params.follower, + host: IsNull(), + }), + Users.findOneBy({ + id: ctx.params.followee, + host: Not(IsNull()), + }), + ]); + + if (follower == null || followee == null) { + ctx.status = 404; + return; + } + + ctx.body = renderActivity(renderFollow(follower, followee)); + ctx.set('Cache-Control', 'public, max-age=180'); + setResponseType(ctx); +}); + export default router; diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index 4d4f733162..beb48713a6 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -1,32 +1,26 @@ import Router from '@koa/router'; +import { FindOptionsWhere, IsNull, LessThan } from 'typeorm'; import config from '@/config/index.js'; -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id.js'; import * as url from '@/prelude/url.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { setResponseType } from '../activitypub.js'; import { Users, Followings, UserProfiles } from '@/models/index.js'; -import { IsNull, LessThan } from 'typeorm'; +import { Following } from '@/models/entities/following.js'; +import { setResponseType } from '../activitypub.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; - // Get 'cursor' parameter - const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor); - - // Get 'page' parameter - const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page); - const page: boolean = ctx.request.query.page === 'true'; - - // Validate parameters - if (cursorErr || pageErr) { + const cursor = ctx.request.query.cursor; + if (cursor != null && typeof cursor !== 'string') { ctx.status = 400; return; } + const page = ctx.request.query.page === 'true'; + const user = await Users.findOneBy({ id: userId, host: IsNull(), @@ -57,7 +51,7 @@ export default async (ctx: Router.RouterContext) => { if (page) { const query = { followeeId: user.id, - } as any; + } as FindOptionsWhere; // ใ‚ซใƒผใ‚ฝใƒซใŒๆŒ‡ๅฎšใ•ใ‚Œใฆใ„ใ‚‹ๅ ดๅˆ if (cursor) { @@ -86,7 +80,7 @@ export default async (ctx: Router.RouterContext) => { inStock ? `${partOf}?${url.query({ page: 'true', cursor: followings[followings.length - 1].id, - })}` : undefined + })}` : undefined, ); ctx.body = renderActivity(rendered); diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index 0af1f424f9..3a25a6316c 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -1,33 +1,26 @@ import Router from '@koa/router'; +import { LessThan, IsNull, FindOptionsWhere } from 'typeorm'; import config from '@/config/index.js'; -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id.js'; import * as url from '@/prelude/url.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { setResponseType } from '../activitypub.js'; import { Users, Followings, UserProfiles } from '@/models/index.js'; -import { LessThan, IsNull, FindOptionsWhere } from 'typeorm'; import { Following } from '@/models/entities/following.js'; +import { setResponseType } from '../activitypub.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; - // Get 'cursor' parameter - const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor); - - // Get 'page' parameter - const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page); - const page: boolean = ctx.request.query.page === 'true'; - - // Validate parameters - if (cursorErr || pageErr) { + const cursor = ctx.request.query.cursor; + if (cursor != null && typeof cursor !== 'string') { ctx.status = 400; return; } + const page = ctx.request.query.page === 'true'; + const user = await Users.findOneBy({ id: userId, host: IsNull(), @@ -87,7 +80,7 @@ export default async (ctx: Router.RouterContext) => { inStock ? `${partOf}?${url.query({ page: 'true', cursor: followings[followings.length - 1].id, - })}` : undefined + })}` : undefined, ); ctx.body = renderActivity(rendered); diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index 6b9592bcf3..7a2586998a 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -1,36 +1,37 @@ import Router from '@koa/router'; +import { Brackets, IsNull } from 'typeorm'; import config from '@/config/index.js'; -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; -import { setResponseType } from '../activitypub.js'; import renderNote from '@/remote/activitypub/renderer/note.js'; import renderCreate from '@/remote/activitypub/renderer/create.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; import { countIf } from '@/prelude/array.js'; import * as url from '@/prelude/url.js'; import { Users, Notes } from '@/models/index.js'; -import { makePaginationQuery } from '../api/common/make-pagination-query.js'; -import { Brackets, IsNull } from 'typeorm'; import { Note } from '@/models/entities/note.js'; +import { makePaginationQuery } from '../api/common/make-pagination-query.js'; +import { setResponseType } from '../activitypub.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.default.optional.type(ID).get(ctx.request.query.since_id); + const sinceId = ctx.request.query.since_id; + if (sinceId != null && typeof sinceId !== 'string') { + ctx.status = 400; + return; + } - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.default.optional.type(ID).get(ctx.request.query.until_id); + const untilId = ctx.request.query.until_id; + if (untilId != null && typeof untilId !== 'string') { + ctx.status = 400; + return; + } - // Get 'page' parameter - const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page); - const page: boolean = ctx.request.query.page === 'true'; + const page = ctx.request.query.page === 'true'; - // Validate parameters - if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) { + if (countIf(x => x != null, [sinceId, untilId]) > 1) { ctx.status = 400; return; } @@ -52,8 +53,8 @@ export default async (ctx: Router.RouterContext) => { const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId) .andWhere('note.userId = :userId', { userId: user.id }) .andWhere(new Brackets(qb => { qb - .where(`note.visibility = 'public'`) - .orWhere(`note.visibility = 'home'`); + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); })) .andWhere('note.localOnly = FALSE'); @@ -76,7 +77,7 @@ export default async (ctx: Router.RouterContext) => { notes.length ? `${partOf}?${url.query({ page: 'true', until_id: notes[notes.length - 1].id, - })}` : undefined + })}` : undefined, ); ctx.body = renderActivity(rendered); @@ -85,7 +86,7 @@ export default async (ctx: Router.RouterContext) => { // index page const rendered = renderOrderedCollection(partOf, user.notesCount, `${partOf}?page=true`, - `${partOf}?page=true&since_id=000000000000000000000000` + `${partOf}?page=true&since_id=000000000000000000000000`, ); ctx.body = renderActivity(rendered); ctx.set('Cache-Control', 'public, max-age=180'); diff --git a/packages/backend/src/server/api/2fa.ts b/packages/backend/src/server/api/2fa.ts index dce8accaac..96b9316e47 100644 --- a/packages/backend/src/server/api/2fa.ts +++ b/packages/backend/src/server/api/2fa.ts @@ -1,6 +1,6 @@ import * as crypto from 'node:crypto'; -import config from '@/config/index.js'; import * as jsrsasign from 'jsrsasign'; +import config from '@/config/index.js'; const ECC_PRELUDE = Buffer.from([0x04]); const NULL_BYTE = Buffer.from([0]); @@ -145,7 +145,7 @@ export function verifyLogin({ export const procedures = { none: { - verify({ publicKey }: {publicKey: Map}) { + verify({ publicKey }: { publicKey: Map }) { const negTwo = publicKey.get(-2); if (!negTwo || negTwo.length !== 32) { diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 9a85e4565b..cd3e0abc06 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -2,10 +2,11 @@ import Koa from 'koa'; import { performance } from 'perf_hooks'; import { limiter } from './limiter.js'; import { CacheableLocalUser, User } from '@/models/entities/user.js'; -import endpoints, { IEndpoint } from './endpoints.js'; +import endpoints, { IEndpointMeta } from './endpoints.js'; import { ApiError } from './error.js'; import { apiLogger } from './logger.js'; import { AccessToken } from '@/models/entities/access-token.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; const accessDenied = { message: 'Access denied.', @@ -15,6 +16,7 @@ const accessDenied = { export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { const isSecure = user != null && token == null; + const isModerator = user != null && (user.isModerator || user.isAdmin); const ep = endpoints.find(e => e.name === endpoint); @@ -31,6 +33,32 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi throw new ApiError(accessDenied); } + if (ep.meta.limit && !isModerator) { + // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. + let limitActor: string; + if (user) { + limitActor = user.id; + } else { + limitActor = getIpHash(ctx!.ip); + } + + const limit = Object.assign({}, ep.meta.limit); + + if (!limit.key) { + limit.key = ep.name; + } + + // Rate limit + await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor).catch(e => { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }); + }); + } + if (ep.meta.requireCredential && user == null) { throw new ApiError({ message: 'Credential required.', @@ -53,7 +81,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); } - if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) { + if (ep.meta.requireModerator && !isModerator) { throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); } @@ -65,18 +93,6 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi }); } - if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) { - // Rate limit - await limiter(ep as IEndpoint & { meta: { limit: NonNullable } }, user!).catch(e => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }); - }); - } - // Cast non JSON input if (ep.meta.requireFile && ep.params.properties) { for (const k of Object.keys(ep.params.properties)) { diff --git a/packages/backend/src/server/api/common/generate-visibility-query.ts b/packages/backend/src/server/api/common/generate-visibility-query.ts index 715982934c..b50b6812f4 100644 --- a/packages/backend/src/server/api/common/generate-visibility-query.ts +++ b/packages/backend/src/server/api/common/generate-visibility-query.ts @@ -3,6 +3,7 @@ import { Followings } from '@/models/index.js'; import { Brackets, SelectQueryBuilder } from 'typeorm'; export function generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null) { + // This code must always be synchronized with the checks in Notes.isVisibleForMe. if (me == null) { q.andWhere(new Brackets(qb => { qb .where(`note.visibility = 'public'`) @@ -11,7 +12,7 @@ export function generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: U } else { const followingQuery = Followings.createQueryBuilder('following') .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); + .where('following.followerId = :meId'); q.andWhere(new Brackets(qb => { qb // ๅ…ฌ้–‹ๆŠ•็จฟใงใ‚ใ‚‹ @@ -20,21 +21,22 @@ export function generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: U .orWhere(`note.visibility = 'home'`); })) // ใพใŸใฏ ่‡ชๅˆ†่‡ช่บซ - .orWhere('note.userId = :userId1', { userId1: me.id }) + .orWhere('note.userId = :meId') // ใพใŸใฏ ่‡ชๅˆ†ๅฎ›ใฆ - .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`) + .orWhere(':meId = ANY(note.visibleUserIds)') + .orWhere(':meId = ANY(note.mentions)') .orWhere(new Brackets(qb => { qb // ใพใŸใฏ ใƒ•ใ‚ฉใƒญใƒฏใƒผๅฎ›ใฆใฎๆŠ•็จฟใงใ‚ใ‚Šใ€ - .where('note.visibility = \'followers\'') + .where(`note.visibility = 'followers'`) .andWhere(new Brackets(qb => { qb // ่‡ชๅˆ†ใŒใƒ•ใ‚ฉใƒญใƒฏใƒผใงใ‚ใ‚‹ .where(`note.userId IN (${ followingQuery.getQuery() })`) // ใพใŸใฏ ่‡ชๅˆ†ใฎๆŠ•็จฟใธใฎใƒชใƒ—ใƒฉใ‚ค - .orWhere('note.replyUserId = :userId3', { userId3: me.id }); + .orWhere('note.replyUserId = :meId'); })); })); })); - q.setParameters(followingQuery.getParameters()); + q.setParameters({ meId: me.id }); } } diff --git a/packages/backend/src/server/api/common/read-messaging-message.ts b/packages/backend/src/server/api/common/read-messaging-message.ts index 3638518e67..c4c18ffa06 100644 --- a/packages/backend/src/server/api/common/read-messaging-message.ts +++ b/packages/backend/src/server/api/common/read-messaging-message.ts @@ -1,6 +1,7 @@ import { publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; import { publishMessagingStream } from '@/services/stream.js'; import { publishMessagingIndexStream } from '@/services/stream.js'; +import { pushNotification } from '@/services/push-notification.js'; import { User, IRemoteUser } from '@/models/entities/user.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js'; @@ -50,6 +51,21 @@ export async function readUserMessagingMessage( if (!await Users.getHasUnreadMessagingMessage(userId)) { // ๅ…จใฆใฎ(ใ„ใพใพใงๆœช่ชญใ ใฃใŸ)่‡ชๅˆ†ๅฎ›ใฆใฎใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’(ใ“ใ‚Œใง)่ชญใฟใพใ—ใŸใ‚ˆใจใ„ใ†ใ‚คใƒ™ใƒณใƒˆใ‚’็™บ่กŒ publishMainStream(userId, 'readAllMessagingMessages'); + pushNotification(userId, 'readAllMessagingMessages', undefined); + } else { + // ใใฎใƒฆใƒผใ‚ถใƒผใจใฎใƒกใƒƒใ‚ปใƒผใ‚ธใงๆœช่ชญใŒใชใ‘ใ‚Œใฐใ‚คใƒ™ใƒณใƒˆ็™บ่กŒ + const count = await MessagingMessages.count({ + where: { + userId: otherpartyId, + recipientId: userId, + isRead: false, + }, + take: 1 + }); + + if (!count) { + pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId }); + } } } @@ -104,6 +120,19 @@ export async function readGroupMessagingMessage( if (!await Users.getHasUnreadMessagingMessage(userId)) { // ๅ…จใฆใฎ(ใ„ใพใพใงๆœช่ชญใ ใฃใŸ)่‡ชๅˆ†ๅฎ›ใฆใฎใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’(ใ“ใ‚Œใง)่ชญใฟใพใ—ใŸใ‚ˆใจใ„ใ†ใ‚คใƒ™ใƒณใƒˆใ‚’็™บ่กŒ publishMainStream(userId, 'readAllMessagingMessages'); + pushNotification(userId, 'readAllMessagingMessages', undefined); + } else { + // ใใฎใ‚ฐใƒซใƒผใƒ—ใซใŠใ„ใฆๆœช่ชญใŒใชใ‘ใ‚Œใฐใ‚คใƒ™ใƒณใƒˆ็™บ่กŒ + const unreadExist = await MessagingMessages.createQueryBuilder('message') + .where(`message.groupId = :groupId`, { groupId: groupId }) + .andWhere('message.userId != :userId', { userId: userId }) + .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) + .andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // ่‡ชๅˆ†ใŒๅŠ ๅ…ฅใ™ใ‚‹ๅ‰ใฎไผš่ฉฑใซใคใ„ใฆใฏใ€ๆœช่ชญๆ‰ฑใ„ใ—ใชใ„ + .getOne().then(x => x != null); + + if (!unreadExist) { + pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId }); + } } } diff --git a/packages/backend/src/server/api/common/read-notification.ts b/packages/backend/src/server/api/common/read-notification.ts index 1f575042a0..0dad35bcc2 100644 --- a/packages/backend/src/server/api/common/read-notification.ts +++ b/packages/backend/src/server/api/common/read-notification.ts @@ -1,4 +1,5 @@ import { publishMainStream } from '@/services/stream.js'; +import { pushNotification } from '@/services/push-notification.js'; import { User } from '@/models/entities/user.js'; import { Notification } from '@/models/entities/notification.js'; import { Notifications, Users } from '@/models/index.js'; @@ -16,28 +17,29 @@ export async function readNotification( isRead: true, }); - post(userId); + if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId); + else return postReadNotifications(userId, notificationIds); } export async function readNotificationByQuery( userId: User['id'], query: Record ) { - // Update documents - await Notifications.update({ + const notificationIds = await Notifications.find({ ...query, notifieeId: userId, isRead: false, - }, { - isRead: true, - }); + }).then(notifications => notifications.map(notification => notification.id)); - post(userId); + return readNotification(userId, notificationIds); } -async function post(userId: User['id']) { - if (!await Users.getHasUnreadNotification(userId)) { - // ๅ…จใฆใฎ(ใ„ใพใพใงๆœช่ชญใ ใฃใŸ)้€š็Ÿฅใ‚’(ใ“ใ‚Œใง)่ชญใฟใพใ—ใŸใ‚ˆใจใ„ใ†ใ‚คใƒ™ใƒณใƒˆใ‚’็™บ่กŒ - publishMainStream(userId, 'readAllNotifications'); - } +function postReadAllNotifications(userId: User['id']) { + publishMainStream(userId, 'readAllNotifications'); + return pushNotification(userId, 'readAllNotifications', undefined); +} + +function postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { + publishMainStream(userId, 'readNotifications', notificationIds); + return pushNotification(userId, 'readNotifications', { notificationIds }); } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e2db03f13a..1e7afd8cdd 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -654,7 +654,6 @@ export interface IEndpointMeta { /** * ใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆใฎใƒชใƒŸใƒ†ใƒผใ‚ทใƒงใƒณใซ้–ขใ™ใ‚‹ใ‚„ใค * ็œ็•ฅใ—ใŸๅ ดๅˆใฏใƒชใƒŸใƒ†ใƒผใ‚ทใƒงใƒณใฏ็„กใ„ใ‚‚ใฎใจใ—ใฆ่งฃ้‡ˆใ•ใ‚Œใพใ™ใ€‚ - * ใพใŸใ€withCredential ใŒ false ใฎๅ ดๅˆใฏใƒชใƒŸใƒ†ใƒผใ‚ทใƒงใƒณใ‚’่กŒใ†ใ“ใจใฏใงใใพใ›ใ‚“ใ€‚ */ readonly limit?: { diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 1d8eb1d618..7a5758d75b 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -1,5 +1,6 @@ -import define from '../../../define.js'; import { Announcements, AnnouncementReads } from '@/models/index.js'; +import { Announcement } from '@/models/entities/announcement.js'; +import define from '../../../define.js'; import { makePaginationQuery } from '../../../common/make-pagination-query.js'; export const meta = { @@ -68,11 +69,21 @@ export default define(meta, paramDef, async (ps) => { const announcements = await query.take(ps.limit).getMany(); + const reads = new Map(); + for (const announcement of announcements) { - (announcement as any).reads = await AnnouncementReads.countBy({ + reads.set(announcement, await AnnouncementReads.countBy({ announcementId: announcement.id, - }); + })); } - return announcements; + return announcements.map(announcement => ({ + id: announcement.id, + createdAt: announcement.createdAt.toISOString(), + updatedAt: announcement.updatedAt?.toISOString() ?? null, + title: announcement.title, + text: announcement.text, + imageUrl: announcement.imageUrl, + reads: reads.get(announcement)!, + })); }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index bf6cc16532..78033aed58 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -1,5 +1,5 @@ +import { Signins, UserProfiles, Users } from '@/models/index.js'; import define from '../../define.js'; -import { Users } from '@/models/index.js'; export const meta = { tags: ['admin'], @@ -23,9 +23,12 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); + const [user, profile] = await Promise.all([ + Users.findOneBy({ id: ps.userId }), + UserProfiles.findOneBy({ userId: ps.userId }) + ]); - if (user == null) { + if (user == null || profile == null) { throw new Error('user not found'); } @@ -34,8 +37,37 @@ export default define(meta, paramDef, async (ps, me) => { throw new Error('cannot show info of admin'); } + if (!_me.isAdmin) { + return { + isModerator: user.isModerator, + isSilenced: user.isSilenced, + isSuspended: user.isSuspended, + }; + } + + const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken']; + Object.keys(profile.integrations).forEach(integration => { + maskedKeys.forEach(key => profile.integrations[integration][key] = ''); + }); + + const signins = await Signins.findBy({ userId: user.id }); + return { - ...user, - token: user.token != null ? '' : user.token, + email: profile.email, + emailVerified: profile.emailVerified, + autoAcceptFollowed: profile.autoAcceptFollowed, + noCrawle: profile.noCrawle, + alwaysMarkNsfw: profile.alwaysMarkNsfw, + carefulBot: profile.carefulBot, + injectFeaturedNote: profile.injectFeaturedNote, + receiveAnnouncementEmail: profile.receiveAnnouncementEmail, + integrations: profile.integrations, + mutedWords: profile.mutedWords, + mutedInstances: profile.mutedInstances, + mutingNotificationTypes: profile.mutingNotificationTypes, + isModerator: user.isModerator, + isSilenced: user.isSilenced, + isSuspended: user.isSuspended, + signins, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 2703b4b9db..1575d81d5d 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Users } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['admin'], @@ -24,8 +24,8 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, - state: { type: 'string', enum: ['all', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: "all" }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, + state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' }, + origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, username: { type: 'string', nullable: true, default: null }, hostname: { type: 'string', 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 b23ee9e3df..09e43301b7 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -27,7 +27,7 @@ export const paramDef = { blockedHosts: { type: 'array', nullable: true, items: { type: 'string', } }, - themeColor: { type: 'string', nullable: true }, + themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, errorImageUrl: { type: 'string', nullable: true }, diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 7ffe89a1e5..415a8cc693 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'read:drive', + description: 'Find the notes to which the given file is attached.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index 80293df5d9..bbae9bf4e4 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -8,6 +8,8 @@ export const meta = { kind: 'read:drive', + description: 'Check if a given file exists.', + res: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 0939ae3365..7397fd9ce9 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -20,6 +20,8 @@ export const meta = { kind: 'write:drive', + description: 'Upload a new drive file.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index 61c56e6314..6108ae7da9 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'write:drive', + description: 'Delete an existing drive file.', + errors: { noSuchFile: { message: 'No such file.', diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index f9b4ea89ea..f2bc7348c6 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import { DriveFiles } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['drive'], @@ -8,6 +8,8 @@ export const meta = { kind: 'read:drive', + description: 'Search for a drive file by a hash of the contents.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index 4938a69d11..245fb45a65 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'read:drive', + description: 'Search for a drive file by the given parameters.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index a2bc0c7aa4..2c604c54c8 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -1,7 +1,7 @@ -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles, Users } from '@/models/index.js'; +import define from '../../../define.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['drive'], @@ -10,6 +10,8 @@ export const meta = { kind: 'read:drive', + description: 'Show the properties of a drive file.', + res: { type: 'object', optional: false, nullable: false, @@ -51,7 +53,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - let file: DriveFile | undefined; + let file: DriveFile | null = null; if (ps.fileId) { file = await DriveFiles.findOneBy({ id: ps.fileId }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 4b3f5f2dc9..e3debe0b4f 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'write:drive', + description: 'Update the properties of a drive file.', + errors: { invalidFileName: { message: 'Invalid file name.', diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index 3bfecac802..53f2298f21 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -13,6 +13,8 @@ export const meta = { max: 60, }, + description: 'Request the server to download a new drive file from the specified URL.', + requireCredential: true, kind: 'write:drive', diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index d5e1b19e54..33f5717728 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -2,8 +2,8 @@ import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; import config from '@/config/index.js'; -import define from '../../../define.js'; import { UserProfiles } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { requireCredential: true, @@ -40,15 +40,17 @@ export default define(meta, paramDef, async (ps, user) => { }); // Get the data URL of the authenticator URL - const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({ + const url = speakeasy.otpauthURL({ secret: secret.base32, encoding: 'base32', label: user.username, issuer: config.host, - })); + }); + const dataUrl = await QRCode.toDataURL(url); return { qr: dataUrl, + url, secret: secret.base32, label: user.username, issuer: config.host, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 9de05918c0..a133294169 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -1,15 +1,15 @@ import ms from 'ms'; +import { In } from 'typeorm'; import create from '@/services/note/create.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; import { User } from '@/models/entities/user.js'; import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { Note } from '@/models/entities/note.js'; -import { noteVisibilities } from '../../../../types.js'; import { Channel } from '@/models/entities/channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { In } from 'typeorm'; +import { noteVisibilities } from '../../../../types.js'; +import { ApiError } from '../../error.js'; +import define from '../../define.js'; export const meta = { tags: ['notes'], @@ -83,7 +83,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { - visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: "public" }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, @@ -134,7 +134,7 @@ export const paramDef = { { // (re)note with text, files and poll are optional properties: { - text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, + text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, }, required: ['text'], }, @@ -149,7 +149,7 @@ export const paramDef = { { // (re)note with poll, text and files are optional properties: { - poll: { type: 'object', nullable: false, }, + poll: { type: 'object', nullable: false }, }, required: ['poll'], }, @@ -172,20 +172,24 @@ export default define(meta, paramDef, async (ps, user) => { let files: DriveFile[] = []; const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; if (fileIds != null) { - files = await DriveFiles.findBy({ - userId: user.id, - id: In(fileIds), - }); + files = await DriveFiles.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: user.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); } - let renote: Note | null; + let renote: Note | null = null; if (ps.renoteId != null) { // Fetch renote to note renote = await Notes.findOneBy({ id: ps.renoteId }); if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.poll) { + } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { throw new ApiError(meta.errors.cannotReRenote); } @@ -201,14 +205,14 @@ export default define(meta, paramDef, async (ps, user) => { } } - let reply: Note | null; + let reply: Note | null = null; if (ps.replyId != null) { // Fetch reply reply = await Notes.findOneBy({ id: ps.replyId }); if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (reply.renoteId && !reply.text && !reply.fileIds && !renote.poll) { + } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { throw new ApiError(meta.errors.cannotReplyToPureRenote); } @@ -234,7 +238,7 @@ export default define(meta, paramDef, async (ps, user) => { } } - let channel: Channel | undefined; + let channel: Channel | null = null; if (ps.channelId != null) { channel = await Channels.findOneBy({ id: ps.channelId }); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 3555424fa6..fbb065329c 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,8 +1,8 @@ +import { DeepPartial, FindOptionsWhere } from 'typeorm'; +import { NoteReactions } from '@/models/index.js'; +import { NoteReaction } from '@/models/entities/note-reaction.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { NoteReactions } from '@/models/index.js'; -import { DeepPartial } from 'typeorm'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; export const meta = { tags: ['notes', 'reactions'], @@ -45,7 +45,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = { noteId: ps.noteId, - } as DeepPartial; + } as FindOptionsWhere; if (ps.type) { // ใƒญใƒผใ‚ซใƒซใƒชใ‚ขใ‚ฏใ‚ทใƒงใƒณใฏใƒ›ใ‚นใƒˆๅใŒ . ใจใ•ใ‚Œใฆใ„ใ‚‹ใŒ diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index c602981b30..5e40e7106f 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,12 +1,12 @@ -import define from '../../define.js'; -import { getNote } from '../../common/getters.js'; -import { ApiError } from '../../error.js'; +import { URLSearchParams } from 'node:url'; import fetch from 'node-fetch'; import config from '@/config/index.js'; import { getAgentByUrl } from '@/misc/fetch.js'; -import { URLSearchParams } from 'node:url'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Notes } from '@/models/index.js'; +import { ApiError } from '../../error.js'; +import { getNote } from '../../common/getters.js'; +import define from '../../define.js'; export const meta = { tags: ['notes'], @@ -80,7 +80,12 @@ export default define(meta, paramDef, async (ps, user) => { agent: getAgentByUrl, }); - const json = await res.json(); + const json = (await res.json()) as { + translations: { + detected_source_language: string; + text: string; + }[]; + }; return { sourceLang: json.translations[0].detected_source_language, diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index abefe07be6..4575cba43f 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,4 +1,5 @@ import { publishMainStream } from '@/services/stream.js'; +import { pushNotification } from '@/services/push-notification.js'; import define from '../../define.js'; import { Notifications } from '@/models/index.js'; @@ -28,4 +29,5 @@ export default define(meta, paramDef, async (ps, user) => { // ๅ…จใฆใฎ้€š็Ÿฅใ‚’่ชญใฟใพใ—ใŸใ‚ˆใจใ„ใ†ใ‚คใƒ™ใƒณใƒˆใ‚’็™บ่กŒ publishMainStream(user.id, 'readAllNotifications'); + pushNotification(user.id, 'readAllNotifications', undefined); }); diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts index c7bc5dc0a5..65e96d4862 100644 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/read.ts @@ -1,10 +1,12 @@ -import { publishMainStream } from '@/services/stream.js'; import define from '../../define.js'; -import { Notifications } from '@/models/index.js'; import { readNotification } from '../../common/read-notification.js'; -import { ApiError } from '../../error.js'; export const meta = { + desc: { + 'ja-JP': '้€š็Ÿฅใ‚’ๆ—ข่ชญใซใ—ใพใ™ใ€‚', + 'en-US': 'Mark a notification as read.' + }, + tags: ['notifications', 'account'], requireCredential: true, @@ -21,23 +23,26 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - notificationId: { type: 'string', format: 'misskey:id' }, - }, - required: ['notificationId'], + oneOf: [ + { + type: 'object', + properties: { + notificationId: { type: 'string', format: 'misskey:id' }, + }, + required: ['notificationId'], + }, + { + type: 'object', + properties: { + notificationIds: { type: 'array', items: { type: 'string', format: 'misskey:id' } }, + }, + required: ['notificationIds'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const notification = await Notifications.findOneBy({ - notifieeId: user.id, - id: ps.notificationId, - }); - - if (notification == null) { - throw new ApiError(meta.errors.noSuchNotification); - } - - readNotification(user.id, [notification.id]); + if ('notificationId' in ps) return readNotification(user.id, [ps.notificationId]); + return readNotification(user.id, ps.notificationIds); }); diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 3dcce8550f..5d37e86b91 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -1,8 +1,8 @@ -import define from '../../define.js'; -import { ApiError } from '../../error.js'; +import { IsNull } from 'typeorm'; import { Pages, Users } from '@/models/index.js'; import { Page } from '@/models/entities/page.js'; -import { IsNull } from 'typeorm'; +import define from '../../define.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['pages'], @@ -45,7 +45,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - let page: Page | undefined; + let page: Page | null = null; if (ps.pageId) { page = await Pages.findOneBy({ id: ps.pageId }); diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 046337f040..12ce7a9834 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -10,8 +10,12 @@ import { genId } from '@/misc/gen-id.js'; import { IsNull } from 'typeorm'; export const meta = { + tags: ['reset password'], + requireCredential: false, + description: 'Request a users password to be reset.', + limit: { duration: ms('1hour'), max: 3, diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index dbe64e9a13..5ff115dab5 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -3,8 +3,12 @@ import { ApiError } from '../error.js'; import { resetDb } from '@/db/postgre.js'; export const meta = { + tags: ['non-productive'], + requireCredential: false, + description: 'Only available when running with NODE_ENV=testing. Reset the database and flush Redis.', + errors: { }, diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 7acc545c40..3dcb0b9b83 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -5,8 +5,12 @@ import { Users, UserProfiles, PasswordResetRequests } from '@/models/index.js'; import { ApiError } from '../error.js'; export const meta = { + tags: ['reset password'], + requireCredential: false, + description: 'Complete the password reset that was previously requested.', + errors: { }, diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index a48973a0df..5bc3b9b6a1 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -8,6 +8,8 @@ export const meta = { requireCredential: true, + description: 'Register to receive push notifications.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index 9748f2a222..c21856d28f 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -5,6 +5,8 @@ export const meta = { tags: ['account'], requireCredential: true, + + description: 'Unregister from receiving push notifications.', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 256da1a66f..9949237a7e 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -1,6 +1,10 @@ import define from '../define.js'; export const meta = { + tags: ['non-productive'], + + description: 'Endpoint for testing input validation.', + requireCredential: false, } as const; diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 424c594749..37d4153950 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -4,6 +4,18 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { tags: ['users', 'clips'], + + description: 'Show all clips this user owns.', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Clip', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 26b1f20df0..b1fb656208 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: false, + description: 'Show everyone that follows this user.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 42cf5216e8..429a5e80e5 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: false, + description: 'Show everyone that this user is following.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index d7c435256c..35bf2df598 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -4,6 +4,18 @@ import { makePaginationQuery } from '../../../common/make-pagination-query.js'; export const meta = { tags: ['users', 'gallery'], + + description: 'Show all gallery posts by the given user.', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'GalleryPost', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 73cadc0df7..ab5837b3f3 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -10,6 +10,8 @@ export const meta = { requireCredential: false, + description: 'Get a list of other users that the specified user frequently replies to.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts index fc775d7cc1..fcaf4af3c3 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Create a new group.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts index f68006994c..1bf253ae3f 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Delete an existing group.', + errors: { noSuchGroup: { message: 'No such group.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts index 75c1acc302..eafd7f592c 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Join a group the authenticated user has been invited to.', + errors: { noSuchInvitation: { message: 'No such invitation.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts index 46bc780ab0..08d3a3804b 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Delete an existing group invitation for the authenticated user without joining the group.', + errors: { noSuchInvitation: { message: 'No such invitation.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts index 30a5beb1d9..cc82e43f21 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts @@ -13,6 +13,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Invite a user to an existing group.', + errors: { noSuchGroup: { message: 'No such group.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts index 77dc59d3e5..6a2862ee5a 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'read:user-groups', + description: 'List the groups that the authenticated user is a member of.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts index 33abd5439f..2343cdf857 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Leave a group. The owner of a group can not leave. They must transfer ownership or delete the group instead.', + errors: { noSuchGroup: { message: 'No such group.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts index b1289e601f..de030193cc 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts @@ -8,6 +8,8 @@ export const meta = { kind: 'read:user-groups', + description: 'List the groups that the authenticated user is the owner of.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts index b31990b2e3..703dad6d3b 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts @@ -10,6 +10,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Removes a specified user from a group. The owner can not be removed.', + errors: { noSuchGroup: { message: 'No such group.', diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts index 3ffb0f5ba9..e1cee5fcf7 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'read:user-groups', + description: 'Show the properties of a group.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts index 41ceee3b2e..1496e766ca 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts @@ -10,6 +10,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Transfer ownership of a group from the authenticated user to another user.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts index 1016aa8926..43cf3e484e 100644 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'write:user-groups', + description: 'Update the properties of a group.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index d5260256d5..d2941a0af5 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -10,6 +10,8 @@ export const meta = { kind: 'write:account', + description: 'Create a new list of users.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index b7ad96eef0..8cd02ee02a 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'write:account', + description: 'Delete an existing list of users.', + errors: { noSuchList: { message: 'No such list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 78311292cb..b337f879b1 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -8,6 +8,8 @@ export const meta = { kind: 'read:account', + description: 'Show all lists that the authenticated user has created.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 76863f07d1..fa7033b02e 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'write:account', + description: 'Remove a user from a list.', + errors: { noSuchList: { message: 'No such list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 260665c63a..1db10afc80 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'write:account', + description: 'Add a user to an existing list.', + errors: { noSuchList: { message: 'No such list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 5f51980e95..94d24e1274 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'read:account', + description: 'Show the properties of a list.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index 52353a14cc..c21cdcf679 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -9,6 +9,8 @@ export const meta = { kind: 'write:account', + description: 'Update the properties of a list.', + res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 16318d2225..57dcdfaa88 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -12,6 +12,8 @@ import { generateMutedInstanceQuery } from '../../common/generate-muted-instance export const meta = { tags: ['users', 'notes'], + description: 'Show all notes that this user created.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index b8b3e8192e..85d122c24f 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -4,6 +4,18 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js'; export const meta = { tags: ['users', 'pages'], + + description: 'Show all pages this user created.', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Page', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index c2d1994343..64994aae49 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -9,6 +9,8 @@ export const meta = { requireCredential: false, + description: 'Show all reactions this user made.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index a8f18de522..6fff94ddcf 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -11,6 +11,8 @@ export const meta = { kind: 'read:account', + description: 'Show users that the authenticated user might be interested to follow.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index c6262122d4..87cab5fcf1 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -6,6 +6,8 @@ export const meta = { requireCredential: true, + description: 'Show the different kinds of relations between the authenticated user and the specified user(s).', + res: { optional: false, nullable: false, oneOf: [ diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 0be385dbbf..c7c7a3f591 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -13,6 +13,8 @@ export const meta = { requireCredential: true, + description: 'File a report.', + errors: { noSuchUser: { message: 'No such user.', diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index f74d80e2ae..6cbf12b3b5 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -9,6 +9,8 @@ export const meta = { requireCredential: false, + description: 'Search for a user by username and/or host.', + res: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index a72a58a843..19c1a2c690 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -8,6 +8,8 @@ export const meta = { requireCredential: false, + description: 'Search for users.', + res: { type: 'array', optional: false, nullable: false, @@ -61,7 +63,14 @@ export default define(meta, paramDef, async (ps, me) => { .getMany(); } else { const nameQuery = Users.createQueryBuilder('user') - .where('user.name ILIKE :query', { query: '%' + ps.query + '%' }) + .where(new Brackets(qb => { + qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }); + + // Also search username if it qualifies as username + if (Users.validateLocalUsername(ps.query)) { + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' }); + } + })) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 183ff1b8bb..b31ca30647 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -11,6 +11,8 @@ export const meta = { requireCredential: false, + description: 'Show the properties of a user.', + res: { optional: false, nullable: false, oneOf: [ diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index d138019a72..d17e8b64b5 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -1,12 +1,15 @@ import define from '../../define.js'; import { ApiError } from '../../error.js'; import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; +import { awaitAll } from '@/prelude/await-all.js'; export const meta = { tags: ['users'], requireCredential: false, + description: 'Show statistics about a user.', + errors: { noSuchUser: { message: 'No such user.', @@ -14,6 +17,94 @@ export const meta = { id: '9e638e45-3b25-4ef7-8f95-07e8498f1819', }, }, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + notesCount: { + type: 'integer', + optional: false, nullable: false, + }, + repliesCount: { + type: 'integer', + optional: false, nullable: false, + }, + renotesCount: { + type: 'integer', + optional: false, nullable: false, + }, + repliedCount: { + type: 'integer', + optional: false, nullable: false, + }, + renotedCount: { + type: 'integer', + optional: false, nullable: false, + }, + pollVotesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pollVotedCount: { + type: 'integer', + optional: false, nullable: false, + }, + localFollowingCount: { + type: 'integer', + optional: false, nullable: false, + }, + remoteFollowingCount: { + type: 'integer', + optional: false, nullable: false, + }, + localFollowersCount: { + type: 'integer', + optional: false, nullable: false, + }, + remoteFollowersCount: { + type: 'integer', + optional: false, nullable: false, + }, + followingCount: { + type: 'integer', + optional: false, nullable: false, + }, + followersCount: { + type: 'integer', + optional: false, nullable: false, + }, + sentReactionsCount: { + type: 'integer', + optional: false, nullable: false, + }, + receivedReactionsCount: { + type: 'integer', + optional: false, nullable: false, + }, + noteFavoritesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pageLikesCount: { + type: 'integer', + optional: false, nullable: false, + }, + pageLikedCount: { + type: 'integer', + optional: false, nullable: false, + }, + driveFilesCount: { + type: 'integer', + optional: false, nullable: false, + }, + driveUsage: { + type: 'integer', + optional: false, nullable: false, + description: 'Drive usage in bytes', + }, + }, + }, } as const; export const paramDef = { @@ -31,109 +122,72 @@ export default define(meta, paramDef, async (ps, me) => { throw new ApiError(meta.errors.noSuchUser); } - const [ - notesCount, - repliesCount, - renotesCount, - repliedCount, - renotedCount, - pollVotesCount, - pollVotedCount, - localFollowingCount, - remoteFollowingCount, - localFollowersCount, - remoteFollowersCount, - sentReactionsCount, - receivedReactionsCount, - noteFavoritesCount, - pageLikesCount, - pageLikedCount, - driveFilesCount, - driveUsage, - ] = await Promise.all([ - Notes.createQueryBuilder('note') + const result = await awaitAll({ + notesCount: Notes.createQueryBuilder('note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - Notes.createQueryBuilder('note') + repliesCount: Notes.createQueryBuilder('note') .where('note.userId = :userId', { userId: user.id }) .andWhere('note.replyId IS NOT NULL') .getCount(), - Notes.createQueryBuilder('note') + renotesCount: Notes.createQueryBuilder('note') .where('note.userId = :userId', { userId: user.id }) .andWhere('note.renoteId IS NOT NULL') .getCount(), - Notes.createQueryBuilder('note') + repliedCount: Notes.createQueryBuilder('note') .where('note.replyUserId = :userId', { userId: user.id }) .getCount(), - Notes.createQueryBuilder('note') + renotedCount: Notes.createQueryBuilder('note') .where('note.renoteUserId = :userId', { userId: user.id }) .getCount(), - PollVotes.createQueryBuilder('vote') + pollVotesCount: PollVotes.createQueryBuilder('vote') .where('vote.userId = :userId', { userId: user.id }) .getCount(), - PollVotes.createQueryBuilder('vote') + pollVotedCount: PollVotes.createQueryBuilder('vote') .innerJoin('vote.note', 'note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - Followings.createQueryBuilder('following') + localFollowingCount: Followings.createQueryBuilder('following') .where('following.followerId = :userId', { userId: user.id }) .andWhere('following.followeeHost IS NULL') .getCount(), - Followings.createQueryBuilder('following') + remoteFollowingCount: Followings.createQueryBuilder('following') .where('following.followerId = :userId', { userId: user.id }) .andWhere('following.followeeHost IS NOT NULL') .getCount(), - Followings.createQueryBuilder('following') + localFollowersCount: Followings.createQueryBuilder('following') .where('following.followeeId = :userId', { userId: user.id }) .andWhere('following.followerHost IS NULL') .getCount(), - Followings.createQueryBuilder('following') + remoteFollowersCount: Followings.createQueryBuilder('following') .where('following.followeeId = :userId', { userId: user.id }) .andWhere('following.followerHost IS NOT NULL') .getCount(), - NoteReactions.createQueryBuilder('reaction') + sentReactionsCount: NoteReactions.createQueryBuilder('reaction') .where('reaction.userId = :userId', { userId: user.id }) .getCount(), - NoteReactions.createQueryBuilder('reaction') + receivedReactionsCount: NoteReactions.createQueryBuilder('reaction') .innerJoin('reaction.note', 'note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - NoteFavorites.createQueryBuilder('favorite') + noteFavoritesCount: NoteFavorites.createQueryBuilder('favorite') .where('favorite.userId = :userId', { userId: user.id }) .getCount(), - PageLikes.createQueryBuilder('like') + pageLikesCount: PageLikes.createQueryBuilder('like') .where('like.userId = :userId', { userId: user.id }) .getCount(), - PageLikes.createQueryBuilder('like') + pageLikedCount: PageLikes.createQueryBuilder('like') .innerJoin('like.page', 'page') .where('page.userId = :userId', { userId: user.id }) .getCount(), - DriveFiles.createQueryBuilder('file') + driveFilesCount: DriveFiles.createQueryBuilder('file') .where('file.userId = :userId', { userId: user.id }) .getCount(), - DriveFiles.calcDriveUsageOf(user), - ]); + driveUsage: DriveFiles.calcDriveUsageOf(user), + }); - return { - notesCount, - repliesCount, - renotesCount, - repliedCount, - renotedCount, - pollVotesCount, - pollVotedCount, - localFollowingCount, - remoteFollowingCount, - localFollowersCount, - remoteFollowersCount, - followingCount: localFollowingCount + remoteFollowingCount, - followersCount: localFollowersCount + remoteFollowersCount, - sentReactionsCount, - receivedReactionsCount, - noteFavoritesCount, - pageLikesCount, - pageLikedCount, - driveFilesCount, - driveUsage, - }; + result.followingCount = result.localFollowingCount + result.remoteFollowingCount; + result.followersCount = result.localFollowersCount + result.remoteFollowersCount; + + return result; }); diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts index e74db8466e..23430cf8b6 100644 --- a/packages/backend/src/server/api/limiter.ts +++ b/packages/backend/src/server/api/limiter.ts @@ -1,25 +1,17 @@ import Limiter from 'ratelimiter'; import { redisClient } from '../../db/redis.js'; -import { IEndpoint } from './endpoints.js'; -import * as Acct from '@/misc/acct.js'; +import { IEndpointMeta } from './endpoints.js'; import { CacheableLocalUser, User } from '@/models/entities/user.js'; import Logger from '@/services/logger.js'; const logger = new Logger('limiter'); -export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable } }, user: CacheableLocalUser) => new Promise((ok, reject) => { - const limitation = endpoint.meta.limit; - - const key = Object.prototype.hasOwnProperty.call(limitation, 'key') - ? limitation.key - : endpoint.name; - - const hasShortTermLimit = - Object.prototype.hasOwnProperty.call(limitation, 'minInterval'); +export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string) => new Promise((ok, reject) => { + const hasShortTermLimit = typeof limitation.minInterval === 'number'; const hasLongTermLimit = - Object.prototype.hasOwnProperty.call(limitation, 'duration') && - Object.prototype.hasOwnProperty.call(limitation, 'max'); + typeof limitation.duration === 'number' && + typeof limitation.max === 'number'; if (hasShortTermLimit) { min(); @@ -32,7 +24,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable { ctx.set('Access-Control-Allow-Origin', config.url); @@ -24,6 +26,21 @@ export default async (ctx: Koa.Context) => { ctx.body = { error }; } + try { + // not more than 1 attempt per second and not more than 10 attempts per hour + await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip)); + } catch (err) { + ctx.status = 429; + ctx.body = { + error: { + message: 'Too many failed attempts to sign in. Try again later.', + code: 'TOO_MANY_AUTHENTICATION_FAILURES', + id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', + }, + }; + return; + } + if (typeof username !== 'string') { ctx.status = 400; return; diff --git a/packages/backend/src/server/api/service/discord.ts b/packages/backend/src/server/api/service/discord.ts index 04197574c2..97cbcbecdb 100644 --- a/packages/backend/src/server/api/service/discord.ts +++ b/packages/backend/src/server/api/service/discord.ts @@ -1,16 +1,16 @@ import Koa from 'koa'; import Router from '@koa/router'; -import { getJson } from '@/misc/fetch.js'; import { OAuth2 } from 'oauth'; +import { v4 as uuid } from 'uuid'; +import { IsNull } from 'typeorm'; +import { getJson } from '@/misc/fetch.js'; import config from '@/config/index.js'; import { publishMainStream } from '@/services/stream.js'; -import { redisClient } from '../../../db/redis.js'; -import { v4 as uuid } from 'uuid'; -import signin from '../common/signin.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Users, UserProfiles } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; -import { IsNull } from 'typeorm'; +import { redisClient } from '../../../db/redis.js'; +import signin from '../common/signin.js'; function getUserToken(ctx: Koa.BaseContext): string | null { return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; @@ -54,7 +54,7 @@ router.get('/disconnect/discord', async ctx => { integrations: profile.integrations, }); - ctx.body = `Discordใฎ้€ฃๆบใ‚’่งฃ้™คใ—ใพใ—ใŸ :v:`; + ctx.body = 'Discordใฎ้€ฃๆบใ‚’่งฃ้™คใ—ใพใ—ใŸ :v:'; // Publish i updated event publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { @@ -140,7 +140,7 @@ router.get('/dc/cb', async ctx => { const code = ctx.query.code; - if (!code) { + if (!code || typeof code !== 'string') { ctx.throw(400, 'invalid session'); return; } @@ -174,17 +174,17 @@ router.get('/dc/cb', async ctx => { } })); - const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { + const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { 'Authorization': `Bearer ${accessToken}`, - }); + })) as Record; - if (!id || !username || !discriminator) { + if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { ctx.throw(400, 'invalid session'); return; } const profile = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'discord'->>'id' = :id`, { id: id }) + .where('"integrations"->\'discord\'->>\'id\' = :id', { id: id }) .andWhere('"userHost" IS NULL') .getOne(); @@ -211,7 +211,7 @@ router.get('/dc/cb', async ctx => { } else { const code = ctx.query.code; - if (!code) { + if (!code || typeof code !== 'string') { ctx.throw(400, 'invalid session'); return; } @@ -245,10 +245,10 @@ router.get('/dc/cb', async ctx => { } })); - const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { + const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { 'Authorization': `Bearer ${accessToken}`, - }); - if (!id || !username || !discriminator) { + })) as Record; + if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { ctx.throw(400, 'invalid session'); return; } diff --git a/packages/backend/src/server/api/service/github.ts b/packages/backend/src/server/api/service/github.ts index 61bb768a63..04dbd1f7ab 100644 --- a/packages/backend/src/server/api/service/github.ts +++ b/packages/backend/src/server/api/service/github.ts @@ -1,16 +1,16 @@ import Koa from 'koa'; import Router from '@koa/router'; -import { getJson } from '@/misc/fetch.js'; import { OAuth2 } from 'oauth'; +import { v4 as uuid } from 'uuid'; +import { IsNull } from 'typeorm'; +import { getJson } from '@/misc/fetch.js'; import config from '@/config/index.js'; import { publishMainStream } from '@/services/stream.js'; -import { redisClient } from '../../../db/redis.js'; -import { v4 as uuid } from 'uuid'; -import signin from '../common/signin.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Users, UserProfiles } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; -import { IsNull } from 'typeorm'; +import { redisClient } from '../../../db/redis.js'; +import signin from '../common/signin.js'; function getUserToken(ctx: Koa.BaseContext): string | null { return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; @@ -54,7 +54,7 @@ router.get('/disconnect/github', async ctx => { integrations: profile.integrations, }); - ctx.body = `GitHubใฎ้€ฃๆบใ‚’่งฃ้™คใ—ใพใ—ใŸ :v:`; + ctx.body = 'GitHubใฎ้€ฃๆบใ‚’่งฃ้™คใ—ใพใ—ใŸ :v:'; // Publish i updated event publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { @@ -138,7 +138,7 @@ router.get('/gh/cb', async ctx => { const code = ctx.query.code; - if (!code) { + if (!code || typeof code !== 'string') { ctx.throw(400, 'invalid session'); return; } @@ -167,16 +167,16 @@ router.get('/gh/cb', async ctx => { } })); - const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { + const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { 'Authorization': `bearer ${accessToken}`, - }); - if (!login || !id) { + })) as Record; + if (typeof login !== 'string' || typeof id !== 'string') { ctx.throw(400, 'invalid session'); return; } const link = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'github'->>'id' = :id`, { id: id }) + .where('"integrations"->\'github\'->>\'id\' = :id', { id: id }) .andWhere('"userHost" IS NULL') .getOne(); @@ -189,7 +189,7 @@ router.get('/gh/cb', async ctx => { } else { const code = ctx.query.code; - if (!code) { + if (!code || typeof code !== 'string') { ctx.throw(400, 'invalid session'); return; } @@ -219,11 +219,11 @@ router.get('/gh/cb', async ctx => { } })); - const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { + const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { 'Authorization': `bearer ${accessToken}`, - }); + })) as Record; - if (!login || !id) { + if (typeof login !== 'string' || typeof id !== 'string') { ctx.throw(400, 'invalid session'); return; } diff --git a/packages/backend/src/server/api/service/twitter.ts b/packages/backend/src/server/api/service/twitter.ts index e72b71e2f7..2b4f9f6daa 100644 --- a/packages/backend/src/server/api/service/twitter.ts +++ b/packages/backend/src/server/api/service/twitter.ts @@ -2,14 +2,14 @@ import Koa from 'koa'; import Router from '@koa/router'; import { v4 as uuid } from 'uuid'; import autwh from 'autwh'; -import { redisClient } from '../../../db/redis.js'; +import { IsNull } from 'typeorm'; import { publishMainStream } from '@/services/stream.js'; import config from '@/config/index.js'; -import signin from '../common/signin.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Users, UserProfiles } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; -import { IsNull } from 'typeorm'; +import signin from '../common/signin.js'; +import { redisClient } from '../../../db/redis.js'; function getUserToken(ctx: Koa.BaseContext): string | null { return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; @@ -53,7 +53,7 @@ router.get('/disconnect/twitter', async ctx => { integrations: profile.integrations, }); - ctx.body = `Twitterใฎ้€ฃๆบใ‚’่งฃ้™คใ—ใพใ—ใŸ :v:`; + ctx.body = 'Twitterใฎ้€ฃๆบใ‚’่งฃ้™คใ—ใพใ—ใŸ :v:'; // Publish i updated event publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { @@ -132,10 +132,16 @@ router.get('/tw/cb', async ctx => { const twCtx = await get; - const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier); + const verifier = ctx.query.oauth_verifier; + if (!verifier || typeof verifier !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const result = await twAuth!.done(JSON.parse(twCtx), verifier); const link = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId }) + .where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId }) .andWhere('"userHost" IS NULL') .getOne(); @@ -148,7 +154,7 @@ router.get('/tw/cb', async ctx => { } else { const verifier = ctx.query.oauth_verifier; - if (verifier == null) { + if (!verifier || typeof verifier !== 'string') { ctx.throw(400, 'invalid session'); return; } diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts index 043d03ab8d..b67600474b 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -1,7 +1,7 @@ -import { default as Xev } from 'xev'; +import Xev from 'xev'; import Channel from '../channel.js'; -const ev = new Xev.default(); +const ev = new Xev(); export default class extends Channel { public readonly chName = 'queueStats'; diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts index 0da1895767..db75a6fa38 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -1,7 +1,7 @@ -import { default as Xev } from 'xev'; +import Xev from 'xev'; import Channel from '../channel.js'; -const ev = new Xev.default(); +const ev = new Xev(); export default class extends Channel { public readonly chName = 'serverStats'; diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index b803478281..2d23145f14 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,27 +1,25 @@ -import * as websocket from 'websocket'; -import { readNotification } from '../common/read-notification.js'; -import call from '../call.js'; -import readNote from '@/services/note/read.js'; -import Channel from './channel.js'; -import channels from './channels/index.js'; import { EventEmitter } from 'events'; +import * as websocket from 'websocket'; +import readNote from '@/services/note/read.js'; import { User } from '@/models/entities/user.js'; import { Channel as ChannelModel } from '@/models/entities/channel.js'; import { Users, Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js'; -import { ApiError } from '../error.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js'; import { UserGroup } from '@/models/entities/user-group.js'; -import { StreamEventEmitter, StreamMessages } from './types.js'; import { Packed } from '@/misc/schema.js'; +import { readNotification } from '../common/read-notification.js'; +import channels from './channels/index.js'; +import Channel from './channel.js'; +import { StreamEventEmitter, StreamMessages } from './types.js'; /** * Main stream connection */ export default class Connection { public user?: User; - public userProfile?: UserProfile; + public userProfile?: UserProfile | null; public following: Set = new Set(); public muting: Set = new Set(); public blocking: Set = new Set(); // "่ขซ"blocking @@ -84,7 +82,7 @@ export default class Connection { this.muting.delete(data.body.id); break; - // TODO: block events + // TODO: block events case 'followChannel': this.followingChannels.add(data.body.id); @@ -126,7 +124,6 @@ export default class Connection { const { type, body } = obj; switch (type) { - case 'api': this.onApiRequest(body); break; case 'readNotification': this.onReadNotification(body); break; case 'subNote': this.onSubscribeNote(body); break; case 's': this.onSubscribeNote(body); break; // alias @@ -183,31 +180,6 @@ export default class Connection { } } - /** - * APIใƒชใ‚ฏใ‚จใ‚นใƒˆ่ฆๆฑ‚ๆ™‚ - */ - private async onApiRequest(payload: any) { - // ๆ–ฐ้ฎฎใชใƒ‡ใƒผใ‚ฟใ‚’ๅˆฉ็”จใ™ใ‚‹ใŸใ‚ใซใƒฆใƒผใ‚ถใƒผใ‚’ใƒ•ใ‚งใƒƒใƒ - const user = this.user ? await Users.findOneBy({ id: this.user.id }) : null; - - const endpoint = payload.endpoint || payload.ep; // alias - - // ๅ‘ผใณๅ‡บใ— - call(endpoint, user, this.token, payload.data).then(res => { - this.sendMessageToWs(`api:${payload.id}`, { res }); - }).catch((e: ApiError) => { - this.sendMessageToWs(`api:${payload.id}`, { - error: { - message: e.message, - code: e.code, - id: e.id, - kind: e.kind, - ...(e.info ? { info: e.info } : {}), - }, - }); - }); - } - private onReadNotification(payload: any) { if (!payload.id) return; readNotification(this.user!.id, [payload.id]); diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index 2a34edac67..f8e42d27fe 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -1,4 +1,4 @@ -import * as http from 'http'; +import * as http from 'node:http'; import * as websocket from 'websocket'; import MainStreamConnection from './stream/index.js'; diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts index 6bc220b362..c34e043145 100644 --- a/packages/backend/src/server/file/send-drive-file.ts +++ b/packages/backend/src/server/file/send-drive-file.ts @@ -4,14 +4,14 @@ import { dirname } from 'node:path'; import Koa from 'koa'; import send from 'koa-send'; import rename from 'rename'; -import * as tmp from 'tmp'; import { serverLogger } from '../index.js'; import { contentDisposition } from '@/misc/content-disposition.js'; import { DriveFiles } from '@/models/index.js'; import { InternalStorage } from '@/services/drive/internal-storage.js'; +import { createTemp } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { detectType } from '@/misc/get-file-info.js'; -import { convertToJpeg, convertToPng, convertToPngOrJpeg } from '@/services/drive/image-processor.js'; +import { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.js'; import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail.js'; import { StatusError } from '@/misc/fetch.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; @@ -50,12 +50,7 @@ export default async function(ctx: Koa.Context) { if (!file.storedInternal) { if (file.isLink && file.uri) { // ๆœŸ้™ๅˆ‡ใ‚Œใƒชใƒขใƒผใƒˆใƒ•ใ‚กใ‚คใƒซ - const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); + const [path, cleanup] = await createTemp(); try { await downloadUrl(file.uri, path); @@ -64,10 +59,8 @@ export default async function(ctx: Koa.Context) { const convertFile = async () => { if (isThumbnail) { - if (['image/jpeg', 'image/webp'].includes(mime)) { - return await convertToJpeg(path, 498, 280); - } else if (['image/png', 'image/svg+xml'].includes(mime)) { - return await convertToPngOrJpeg(path, 498, 280); + if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(mime)) { + return await convertToWebp(path, 498, 280); } else if (mime.startsWith('video/')) { return await GenerateVideoThumbnail(path); } diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index b50e38a63b..f31de2b7f4 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -2,8 +2,9 @@ * Core Server */ +import cluster from 'node:cluster'; import * as fs from 'node:fs'; -import * as http from 'http'; +import * as http from 'node:http'; import Koa from 'koa'; import Router from '@koa/router'; import mount from 'koa-mount'; @@ -88,10 +89,10 @@ router.get('/avatar/@:acct', async ctx => { }); router.get('/identicon/:x', async ctx => { - const [temp] = await createTemp(); + const [temp, cleanup] = await createTemp(); await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); ctx.set('Content-Type', 'image/png'); - ctx.body = fs.createReadStream(temp); + ctx.body = fs.createReadStream(temp).on('close', () => cleanup()); }); router.get('/verify-email/:code', async ctx => { @@ -142,5 +143,26 @@ export default () => new Promise(resolve => { initializeStreamingServer(server); + server.on('error', e => { + switch ((e as any).code) { + case 'EACCES': + serverLogger.error(`You do not have permission to listen on port ${config.port}.`); + break; + case 'EADDRINUSE': + serverLogger.error(`Port ${config.port} is already in use by another process.`); + break; + default: + serverLogger.error(e); + break; + } + + if (cluster.isWorker) { + process.send!('listenFailed'); + } else { + // disableClustering + process.exit(1); + } + }); + server.listen(config.port, resolve); }); diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts index 3cc5b827a6..48887bf12f 100644 --- a/packages/backend/src/server/proxy/proxy-media.ts +++ b/packages/backend/src/server/proxy/proxy-media.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import Koa from 'koa'; import { serverLogger } from '../index.js'; -import { IImage, convertToPng, convertToJpeg } from '@/services/drive/image-processor.js'; +import { IImage, convertToWebp } from '@/services/drive/image-processor.js'; import { createTemp } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { detectType } from '@/misc/get-file-info.js'; @@ -27,11 +27,11 @@ export async function proxyMedia(ctx: Koa.Context) { let image: IImage; if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime)) { - image = await convertToPng(path, 498, 280); + image = await convertToWebp(path, 498, 280); } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/svg+xml'].includes(mime)) { - image = await convertToJpeg(path, 200, 200); + image = await convertToWebp(path, 200, 200); } else if (['image/svg+xml'].includes(mime)) { - image = await convertToPng(path, 2048, 2048); + image = await convertToWebp(path, 2048, 2048, 1); } else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { throw new StatusError('Rejected type', 403, 'Rejected type'); } else { diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 751e8619bf..94329e11c9 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -54,19 +54,11 @@ //#endregion //#region Script - const salt = localStorage.getItem('salt') - ? `?salt=${localStorage.getItem('salt')}` - : ''; - - const script = document.createElement('script'); - script.setAttribute('src', `/assets/app.${v}.js${salt}`); - script.setAttribute('async', 'true'); - script.setAttribute('defer', 'true'); - script.addEventListener('error', async () => { - await checkUpdate(); - renderError('APP_FETCH_FAILED'); - }); - document.head.appendChild(script); + import(`/assets/${CLIENT_ENTRY}`) + .catch(async e => { + await checkUpdate(); + renderError('APP_FETCH_FAILED', JSON.stringify(e)); + }) //#endregion //#region Theme @@ -146,9 +138,6 @@ // eslint-disable-next-line no-inner-declarations function refresh() { - // Random - localStorage.setItem('salt', Math.random().toString().substr(2, 8)); - // Clear cache (service worker) try { navigator.serviceWorker.controller.postMessage('clear'); diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 34d56cfd0c..2feee72be7 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -4,6 +4,7 @@ import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { PathOrFileDescriptor, readFileSync } from 'node:fs'; import ms from 'ms'; import Koa from 'koa'; import Router from '@koa/router'; @@ -14,7 +15,7 @@ import { createBullBoard } from '@bull-board/api'; import { BullAdapter } from '@bull-board/api/bullAdapter.js'; import { KoaAdapter } from '@bull-board/koa'; -import { IsNull } from 'typeorm'; +import { In, IsNull } from 'typeorm'; import { fetchMeta } from '@/misc/fetch-meta.js'; import config from '@/config/index.js'; import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index.js'; @@ -32,6 +33,7 @@ const _dirname = dirname(_filename); const staticAssets = `${_dirname}/../../../assets/`; const clientAssets = `${_dirname}/../../../../client/assets/`; const assets = `${_dirname}/../../../../../built/_client_dist_/`; +const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; // Init app const app = new Koa(); @@ -72,6 +74,9 @@ app.use(views(_dirname + '/views', { extension: 'pug', options: { version: config.version, + getClientEntry: () => process.env.NODE_ENV === 'production' ? + config.clientEntry : + JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'], config, }, })); @@ -136,9 +141,10 @@ router.get('/twemoji/(.*)', async ctx => { }); // ServiceWorker -router.get('/sw.js', async ctx => { - await send(ctx as any, `/sw.${config.version}.js`, { - root: assets, +router.get(`/sw.js`, async ctx => { + await send(ctx as any, `/sw.js`, { + root: swAssets, + maxage: ms('10 minutes'), }); }); @@ -241,7 +247,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { icon: meta.iconUrl, themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=30'); + ctx.set('Cache-Control', 'public, max-age=15'); } else { // ใƒชใƒขใƒผใƒˆใƒฆใƒผใ‚ถใƒผใชใฎใง // ใƒขใƒ‡ใƒฌใƒผใ‚ฟใŒAPI็ตŒ็”ฑใงๅ‚็…งๅฏ่ƒฝใซใ™ใ‚‹ใŸใ‚ใซ404ใซใฏใ—ใชใ„ @@ -266,7 +272,10 @@ router.get('/users/:user', async ctx => { // Note router.get('/notes/:note', async (ctx, next) => { - const note = await Notes.findOneBy({ id: ctx.params.note }); + const note = await Notes.findOneBy({ + id: ctx.params.note, + visibility: In(['public', 'home']), + }); if (note) { const _note = await Notes.pack(note); @@ -283,11 +292,7 @@ router.get('/notes/:note', async (ctx, next) => { themeColor: meta.themeColor, }); - if (['public', 'home'].includes(note.visibility)) { - ctx.set('Cache-Control', 'public, max-age=180'); - } else { - ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); - } + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -324,7 +329,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => { }); if (['public'].includes(page.visibility)) { - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); } else { ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); } @@ -355,7 +360,7 @@ router.get('/clips/:clip', async (ctx, next) => { themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -380,7 +385,7 @@ router.get('/gallery/:post', async (ctx, next) => { themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -404,7 +409,7 @@ router.get('/channels/:channel', async (ctx, next) => { themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -463,7 +468,7 @@ router.get('(.*)', async ctx => { icon: meta.iconUrl, themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=300'); + ctx.set('Cache-Control', 'public, max-age=15'); }); // Register router diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts index bcbf9b76a7..ee568b8077 100644 --- a/packages/backend/src/server/web/manifest.ts +++ b/packages/backend/src/server/web/manifest.ts @@ -1,16 +1,18 @@ import Koa from 'koa'; -import manifest from './manifest.json' assert { type: 'json' }; import { fetchMeta } from '@/misc/fetch-meta.js'; +import manifest from './manifest.json' assert { type: 'json' }; export const manifestHandler = async (ctx: Koa.Context) => { - const json = JSON.parse(JSON.stringify(manifest)); + // TODO + //const res = structuredClone(manifest); + const res = JSON.parse(JSON.stringify(manifest)); const instance = await fetchMeta(true); - json.short_name = instance.name || 'Misskey'; - json.name = instance.name || 'Misskey'; - if (instance.themeColor) json.theme_color = instance.themeColor; + res.short_name = instance.name || 'Misskey'; + res.name = instance.name || 'Misskey'; + if (instance.themeColor) res.theme_color = instance.themeColor; ctx.set('Cache-Control', 'max-age=300'); - ctx.body = json; + ctx.body = res; }; diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 9c4cd4b9bf..d59f00fe16 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -39,28 +39,24 @@ html { width: 28px; height: 28px; transform: translateY(70px); + color: var(--accent); } - -#splashSpinner:before, -#splashSpinner:after { - content: " "; - display: block; - box-sizing: border-box; - width: 28px; - height: 28px; - border-radius: 50%; - border: solid 4px; -} - -#splashSpinner:before { - border-color: currentColor; - opacity: 0.3; -} - -#splashSpinner:after { +#splashSpinner > .spinner { position: absolute; top: 0; - border-color: currentColor transparent transparent transparent; + left: 0; + width: 28px; + height: 28px; + fill-rule: evenodd; + clip-rule: evenodd; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 1.5; +} +#splashSpinner > .spinner.bg { + opacity: 0.275; +} +#splashSpinner > .spinner.fg { animation: splashSpinner 0.5s linear infinite; } diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts index 6bd8ead5b5..1e259649f9 100644 --- a/packages/backend/src/server/web/url-preview.ts +++ b/packages/backend/src/server/web/url-preview.ts @@ -56,7 +56,7 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => { function wrap(url?: string): string | null { return url != null ? url.match(/^https?:\/\//) - ? `${config.url}/proxy/preview.jpg?${query({ + ? `${config.url}/proxy/preview.webp?${query({ url, preview: '1', })}` diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 1513208310..5bb156f0f4 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -1,17 +1,21 @@ block vars +block loadClientEntry + - const clientEntry = getClientEntry(); + doctype html -!= '\n' +// + - + _____ _ _ + | |_|___ ___| |_ ___ _ _ + | | | | |_ -|_ -| '_| -_| | | + |_|_|_|_|___|___|_,_|___|_ | + |___| + Thank you for using Misskey! + If you are reading this message... how about joining the development? + https://github.com/misskey-dev/misskey + html @@ -30,8 +34,14 @@ html link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') - link(rel='preload' href='/assets/fontawesome/css/all.css' as='style') link(rel='stylesheet' href='/assets/fontawesome/css/all.css') + link(rel='modulepreload' href=`/assets/${clientEntry.file}`) + + each href in clientEntry.css + link(rel='preload' href=`/assets/${href}` as='style') + + each href in clientEntry.css + link(rel='preload' href=`/assets/${href}` as='style') title block title @@ -50,6 +60,10 @@ html style include ../style.css + script. + var VERSION = "#{version}"; + var CLIENT_ENTRY = "#{clientEntry.file}"; + script include ../boot.js @@ -61,4 +75,14 @@ html div#splash img#splashIcon(src= icon || '/static-assets/splash.png') div#splashSpinner + + + + + + + + + + block content diff --git a/packages/backend/src/server/well-known.ts b/packages/backend/src/server/well-known.ts index 7530b4e0ba..1d094f2edd 100644 --- a/packages/backend/src/server/well-known.ts +++ b/packages/backend/src/server/well-known.ts @@ -41,6 +41,7 @@ router.options(allPath, async ctx => { router.get('/.well-known/host-meta', async ctx => { ctx.set('Content-Type', xrd); ctx.body = XRD({ element: 'Link', attributes: { + rel: 'lrdd', type: xrd, template: `${config.url}${webFingerPath}?resource={uri}`, } }); diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts index 5e96e5037f..a2c61cca22 100644 --- a/packages/backend/src/services/blocking/create.ts +++ b/packages/backend/src/services/blocking/create.ts @@ -2,9 +2,10 @@ import { publishMainStream, publishUserEvent } from '@/services/stream.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; -import renderBlock from '@/remote/activitypub/renderer/block.js'; +import { renderBlock } from '@/remote/activitypub/renderer/block.js'; import { deliver } from '@/queue/index.js'; import renderReject from '@/remote/activitypub/renderer/reject.js'; +import { Blocking } from '@/models/entities/blocking.js'; import { User } from '@/models/entities/user.js'; import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js'; import { perUserFollowingChart } from '@/services/chart/index.js'; @@ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) { removeFromList(blockee, blocker), ]); - await Blockings.insert({ + const blocking = { id: genId(), createdAt: new Date(), + blocker, blockerId: blocker.id, + blockee, blockeeId: blockee.id, - }); + } as Blocking; + + await Blockings.insert(blocking); if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { - const content = renderActivity(renderBlock(blocker, blockee)); + const content = renderActivity(renderBlock(blocking)); deliver(blocker, content, blockee.inbox); } } @@ -95,17 +100,12 @@ async function unFollow(follower: User, followee: User) { return; } - Followings.delete(following.id); - - //#region Decrement following count - Users.decrement({ id: follower.id }, 'followingCount', 1); - //#endregion - - //#region Decrement followers count - Users.decrement({ id: followee.id }, 'followersCount', 1); - //#endregion - - perUserFollowingChart.update(follower, followee, false); + await Promise.all([ + Followings.delete(following.id), + Users.decrement({ id: follower.id }, 'followingCount', 1), + Users.decrement({ id: followee.id }, 'followersCount', 1), + perUserFollowingChart.update(follower, followee, false), + ]); // Publish unfollow event if (Users.isLocalUser(follower)) { diff --git a/packages/backend/src/services/blocking/delete.ts b/packages/backend/src/services/blocking/delete.ts index d7b5ddd5ff..cb16651bc0 100644 --- a/packages/backend/src/services/blocking/delete.ts +++ b/packages/backend/src/services/blocking/delete.ts @@ -1,5 +1,5 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import renderBlock from '@/remote/activitypub/renderer/block.js'; +import { renderBlock } from '@/remote/activitypub/renderer/block.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; import { deliver } from '@/queue/index.js'; import Logger from '../logger.js'; @@ -19,11 +19,16 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) { return; } + // Since we already have the blocker and blockee, we do not need to fetch + // them in the query above and can just manually insert them here. + blocking.blocker = blocker; + blocking.blockee = blockee; + Blockings.delete(blocking.id); // deliver if remote bloking if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { - const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker)); + const content = renderActivity(renderUndo(renderBlock(blocking), blocker)); deliver(blocker, content, blockee.inbox); } } diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts index cf69e2194d..2960bac8f7 100644 --- a/packages/backend/src/services/chart/core.ts +++ b/packages/backend/src/services/chart/core.ts @@ -91,27 +91,20 @@ type ToJsonSchema = { }; export function getJsonSchema(schema: S): ToJsonSchema>> { - const object = {}; - for (const [k, v] of Object.entries(schema)) { - nestedProperty.set(object, k, null); - } + const jsonSchema = { + type: 'object', + properties: {} as Record, + required: [], + }; - function f(obj: Record>) { - const jsonSchema = { - type: 'object', - properties: {} as Record, - required: [], + for (const k in schema) { + jsonSchema.properties[k] = { + type: 'array', + items: { type: 'number' }, }; - for (const [k, v] of Object.entries(obj)) { - jsonSchema.properties[k] = v === null ? { - type: 'array', - items: { type: 'number' }, - } : f(v as Record>); - } - return jsonSchema; } - return f(object) as ToJsonSchema>>; + return jsonSchema as ToJsonSchema>>; } /** diff --git a/packages/backend/src/services/chart/entities.ts b/packages/backend/src/services/chart/entities.ts index 13e994cb65..a9eeabd639 100644 --- a/packages/backend/src/services/chart/entities.ts +++ b/packages/backend/src/services/chart/entities.ts @@ -11,6 +11,11 @@ import { entity as PerUserFollowingChart } from './charts/entities/per-user-foll import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; import { entity as ApRequestChart } from './charts/entities/ap-request.js'; +import { entity as TestChart } from './charts/entities/test.js'; +import { entity as TestGroupedChart } from './charts/entities/test-grouped.js'; +import { entity as TestUniqueChart } from './charts/entities/test-unique.js'; +import { entity as TestIntersectionChart } from './charts/entities/test-intersection.js'; + export const entities = [ FederationChart.hour, FederationChart.day, NotesChart.hour, NotesChart.day, @@ -24,4 +29,11 @@ export const entities = [ PerUserFollowingChart.hour, PerUserFollowingChart.day, PerUserDriveChart.hour, PerUserDriveChart.day, ApRequestChart.hour, ApRequestChart.day, + + ...(process.env.NODE_ENV === 'test' ? [ + TestChart.hour, TestChart.day, + TestGroupedChart.hour, TestGroupedChart.day, + TestUniqueChart.hour, TestUniqueChart.day, + TestIntersectionChart.hour, TestIntersectionChart.day, + ] : []), ]; diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts index 9a53db1f38..d53a4235b8 100644 --- a/packages/backend/src/services/create-notification.ts +++ b/packages/backend/src/services/create-notification.ts @@ -1,5 +1,5 @@ import { publishMainStream } from '@/services/stream.js'; -import pushSw from './push-notification.js'; +import { pushNotification } from '@/services/push-notification.js'; import { Notifications, Mutings, UserProfiles, Users } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { User } from '@/models/entities/user.js'; @@ -52,8 +52,8 @@ export async function createNotification( //#endregion publishMainStream(notifieeId, 'unreadNotification', packed); + pushNotification(notifieeId, 'notification', packed); - pushSw(notifieeId, 'notification', packed); if (type === 'follow') sendEmailNotification.follow(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); }, 2000); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 549b11c9fe..cfbcb60ddf 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -7,7 +7,7 @@ import { deleteFile } from './delete-file.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { GenerateVideoThumbnail } from './generate-video-thumbnail.js'; import { driveLogger } from './logger.js'; -import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng, convertSharpToPngOrJpeg } from './image-processor.js'; +import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js'; import { contentDisposition } from '@/misc/content-disposition.js'; import { getFileInfo } from '@/misc/get-file-info.js'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js'; @@ -179,6 +179,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool } let img: sharp.Sharp | null = null; + let satisfyWebpublic: boolean; try { img = sharp(path); @@ -192,6 +193,13 @@ export async function generateAlts(path: string, type: string, generateWeb: bool thumbnail: null, }; } + + satisfyWebpublic = !!( + type !== 'image/svg+xml' && type !== 'image/webp' && + !(metadata.exif || metadata.iptc || metadata.xmp || metadata.tifftagPhotoshop) && + metadata.width && metadata.width <= 2048 && + metadata.height && metadata.height <= 2048 + ); } catch (err) { logger.warn(`sharp failed: ${err}`); return { @@ -203,15 +211,15 @@ export async function generateAlts(path: string, type: string, generateWeb: bool // #region webpublic let webpublic: IImage | null = null; - if (generateWeb) { + if (generateWeb && !satisfyWebpublic) { logger.info(`creating web image`); try { - if (['image/jpeg'].includes(type)) { + if (['image/jpeg', 'image/webp'].includes(type)) { webpublic = await convertSharpToJpeg(img, 2048, 2048); - } else if (['image/webp'].includes(type)) { - webpublic = await convertSharpToWebp(img, 2048, 2048); - } else if (['image/png', 'image/svg+xml'].includes(type)) { + } else if (['image/png'].includes(type)) { + webpublic = await convertSharpToPng(img, 2048, 2048); + } else if (['image/svg+xml'].includes(type)) { webpublic = await convertSharpToPng(img, 2048, 2048); } else { logger.debug(`web image not created (not an required image)`); @@ -220,7 +228,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool logger.warn(`web image not created (an error occured)`, err as Error); } } else { - logger.info(`web image not created (from remote)`); + if (satisfyWebpublic) logger.info(`web image not created (original satisfies webpublic)`); + else logger.info(`web image not created (from remote)`); } // #endregion webpublic @@ -228,10 +237,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool let thumbnail: IImage | null = null; try { - if (['image/jpeg', 'image/webp'].includes(type)) { - thumbnail = await convertSharpToJpeg(img, 498, 280); - } else if (['image/png', 'image/svg+xml'].includes(type)) { - thumbnail = await convertSharpToPngOrJpeg(img, 498, 280); + if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) { + thumbnail = await convertSharpToWebp(img, 498, 280); } else { logger.debug(`thumbnail not created (not an required file)`); } diff --git a/packages/backend/src/services/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts index 04a7a83346..ca12ab8d3d 100644 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ b/packages/backend/src/services/drive/generate-video-thumbnail.ts @@ -1,37 +1,31 @@ import * as fs from 'node:fs'; -import * as tmp from 'tmp'; +import * as path from 'node:path'; +import { createTemp } from '@/misc/create-temp.js'; import { IImage, convertToJpeg } from './image-processor.js'; -import * as FFmpeg from 'fluent-ffmpeg'; +import FFmpeg from 'fluent-ffmpeg'; -export async function GenerateVideoThumbnail(path: string): Promise { - const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => { - tmp.dir((e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); +export async function GenerateVideoThumbnail(source: string): Promise { + const [file, cleanup] = await createTemp(); + const parsed = path.parse(file); + + try { + await new Promise((res, rej) => { + FFmpeg({ + source, + }) + .on('end', res) + .on('error', rej) + .screenshot({ + folder: parsed.dir, + filename: parsed.base, + count: 1, + timestamps: ['5%'], + }); }); - }); - await new Promise((res, rej) => { - FFmpeg({ - source: path, - }) - .on('end', res) - .on('error', rej) - .screenshot({ - folder: outDir, - filename: 'output.png', - count: 1, - timestamps: ['5%'], - }); - }); - - const outPath = `${outDir}/output.png`; - - const thumbnail = await convertToJpeg(outPath, 498, 280); - - // cleanup - await fs.promises.unlink(outPath); - cleanup(); - - return thumbnail; + // JPEGใซๅค‰ๆ› (Webpใงใ‚‚ใ„ใ„ใŒใ€MastodonใฏWebpใ‚’ใ‚ตใƒใƒผใƒˆใ›ใš่กจ็คบใงใใชใใชใ‚‹) + return await convertToJpeg(498, 280); + } finally { + cleanup(); + } } diff --git a/packages/backend/src/services/drive/image-processor.ts b/packages/backend/src/services/drive/image-processor.ts index 146dcfb6ca..2c564ea595 100644 --- a/packages/backend/src/services/drive/image-processor.ts +++ b/packages/backend/src/services/drive/image-processor.ts @@ -38,11 +38,11 @@ export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, heig * Convert to WebP * with resize, remove metadata, resolve orientation, stop animation */ -export async function convertToWebp(path: string, width: number, height: number): Promise { - return convertSharpToWebp(await sharp(path), width, height); +export async function convertToWebp(path: string, width: number, height: number, quality: number = 85): Promise { + return convertSharpToWebp(await sharp(path), width, height, quality); } -export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number): Promise { +export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality: number = 85): Promise { const data = await sharp .resize(width, height, { fit: 'inside', @@ -50,7 +50,7 @@ export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, heig }) .rotate() .webp({ - quality: 85, + quality, }) .toBuffer(); @@ -85,23 +85,3 @@ export async function convertSharpToPng(sharp: sharp.Sharp, width: number, heigh type: 'image/png', }; } - -/** - * Convert to PNG or JPEG - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToPngOrJpeg(path: string, width: number, height: number): Promise { - return convertSharpToPngOrJpeg(await sharp(path), width, height); -} - -export async function convertSharpToPngOrJpeg(sharp: sharp.Sharp, width: number, height: number): Promise { - const stats = await sharp.stats(); - const metadata = await sharp.metadata(); - - // ไธ้€ๆ˜Žใง300x300pxใฎ็ฏ„ๅ›ฒใ‚’่ถ…ใˆใฆใ„ใ‚ŒใฐJPEG - if (stats.isOpaque && ((metadata.width && metadata.width >= 300) || (metadata.height && metadata!.height >= 300))) { - return await convertSharpToJpeg(sharp, width, height); - } else { - return await convertSharpToPng(sharp, width, height); - } -} diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index 79b1b8c2e1..001fc49ee4 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -45,29 +45,20 @@ export async function uploadFromUrl({ // Create temp file const [path, cleanup] = await createTemp(); - // write content at URL to temp file - await downloadUrl(url, path); - - let driveFile: DriveFile; - let error; - try { - driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive }); + // write content at URL to temp file + await downloadUrl(url, path); + + const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive }); logger.succ(`Got: ${driveFile.id}`); + return driveFile!; } catch (e) { - error = e; logger.error(`Failed to create drive file: ${e}`, { url: url, e: e, }); - } - - // clean-up - cleanup(); - - if (error) { - throw error; - } else { - return driveFile!; + throw e; + } finally { + cleanup(); } } diff --git a/packages/backend/src/services/fetch-instance-metadata.ts b/packages/backend/src/services/fetch-instance-metadata.ts index d5294c5fe8..029c388dc2 100644 --- a/packages/backend/src/services/fetch-instance-metadata.ts +++ b/packages/backend/src/services/fetch-instance-metadata.ts @@ -1,5 +1,6 @@ import { DOMWindow, JSDOM } from 'jsdom'; import fetch from 'node-fetch'; +import tinycolor from 'tinycolor2'; import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js'; import { Instance } from '@/models/entities/instance.js'; import { Instances } from '@/models/index.js'; @@ -208,16 +209,11 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul } async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record | null): Promise { - if (doc) { - const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content'); + const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color; - if (themeColor) { - return themeColor; - } - } - - if (manifest) { - return manifest.theme_color; + if (themeColor) { + const color = new tinycolor(themeColor); + if (color.isValid()) return color.toHexString(); } return null; diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts index 7491c44f83..72c24676bb 100644 --- a/packages/backend/src/services/following/create.ts +++ b/packages/backend/src/services/following/create.ts @@ -67,8 +67,10 @@ export async function insertFollowingDoc(followee: { id: User['id']; host: User[ if (alreadyFollowed) return; //#region Increment counts - Users.increment({ id: follower.id }, 'followingCount', 1); - Users.increment({ id: followee.id }, 'followersCount', 1); + await Promise.all([ + Users.increment({ id: follower.id }, 'followingCount', 1), + Users.increment({ id: followee.id }, 'followersCount', 1), + ]); //#endregion //#region Update instance stats diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts index 241f9606e5..91b5a3d61d 100644 --- a/packages/backend/src/services/following/delete.ts +++ b/packages/backend/src/services/following/delete.ts @@ -58,12 +58,11 @@ export default async function(follower: { id: User['id']; host: User['host']; ur } export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) { - //#region Decrement following count - Users.decrement({ id: follower.id }, 'followingCount', 1); - //#endregion - - //#region Decrement followers count - Users.decrement({ id: followee.id }, 'followersCount', 1); + //#region Decrement following / followers counts + await Promise.all([ + Users.decrement({ id: follower.id }, 'followingCount', 1), + Users.decrement({ id: followee.id }, 'followersCount', 1), + ]); //#endregion //#region Update instance stats diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index e5cd5a30d2..e6b3204922 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -5,7 +5,7 @@ import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/i import { genId } from '@/misc/gen-id.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; -import pushNotification from '../push-notification.js'; +import { pushNotification } from '@/services/push-notification.js'; import { Not } from 'typeorm'; import { Note } from '@/models/entities/note.js'; import renderNote from '@/remote/activitypub/renderer/note.js'; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index f14bc2059b..e2bf9d5b59 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -187,6 +187,8 @@ export default async (user: { id: User['id']; username: User['username']; host: if (data.text) { data.text = data.text.trim(); + } else { + data.text = null; } let tags = data.apHashtags; @@ -310,7 +312,8 @@ export default async (user: { id: User['id']; username: User['username']; host: endedPollNotificationQueue.add({ noteId: note.id, }, { - delay + delay, + removeOnComplete: true, }); } diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index ffd609dd84..4963200161 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,3 +1,4 @@ +import { Brackets, In } from 'typeorm'; import { publishNoteStream } from '@/services/stream.js'; import renderDelete from '@/remote/activitypub/renderer/delete.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; @@ -5,15 +6,14 @@ import renderUndo from '@/remote/activitypub/renderer/undo.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderTombstone from '@/remote/activitypub/renderer/tombstone.js'; import config from '@/config/index.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { Notes, Users, Instances } from '@/models/index.js'; import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js'; import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager.js'; import { countSameRenotes } from '@/misc/count-same-renotes.js'; +import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { deliverToRelays } from '../relay.js'; -import { Brackets, In } from 'typeorm'; /** * ๆŠ•็จฟใ‚’ๅ‰Š้™คใ—ใพใ™ใ€‚ @@ -40,7 +40,7 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us //#region ใƒญใƒผใ‚ซใƒซใฎๆŠ•็จฟใชใ‚‰ๅ‰Š้™คใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ“ใƒ†ใ‚ฃใ‚’้…้€ if (Users.isLocalUser(user) && !note.localOnly) { - let renote: Note | null; + let renote: Note | null = null; // if deletd note is renote if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { @@ -113,7 +113,7 @@ async function getMentionedRemoteUsers(note: Note) { const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); if (uris.length > 0) { where.push( - { uri: In(uris) } + { uri: In(uris) }, ); } diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts index 5a0948bca9..83d302826a 100644 --- a/packages/backend/src/services/note/reaction/create.ts +++ b/packages/backend/src/services/note/reaction/create.ts @@ -27,6 +27,11 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note, } } + // check visibility + if (!await Notes.isVisibleForMe(note, user.id)) { + throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); + } + // TODO: cache reaction = await toDbReaction(reaction, user.host); diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts index 41122c92e8..5c3bafbb34 100644 --- a/packages/backend/src/services/push-notification.ts +++ b/packages/backend/src/services/push-notification.ts @@ -5,8 +5,15 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; import { Packed } from '@/misc/schema.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -type notificationType = 'notification' | 'unreadMessagingMessage'; -type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>; +// Defined also packages/sw/types.ts#L14-L21 +type pushNotificationsTypes = { + 'notification': Packed<'Notification'>; + 'unreadMessagingMessage': Packed<'MessagingMessage'>; + 'readNotifications': { notificationIds: string[] }; + 'readAllNotifications': undefined; + 'readAllMessagingMessages': undefined; + 'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string }; +}; // ใƒ—ใƒƒใ‚ทใƒฅใƒกใƒƒใ‚ปใƒผใ‚ธใ‚ตใƒผใƒใƒผใซใฏๆ–‡ๅญ—ๆ•ฐๅˆถ้™ใŒใ‚ใ‚‹ใŸใ‚ใ€ๅ†…ๅฎนใ‚’ๅ‰Šๆธ›ใ—ใพใ™ function truncateNotification(notification: Packed<'Notification'>): any { @@ -17,12 +24,11 @@ function truncateNotification(notification: Packed<'Notification'>): any { ...notification.note, // textใ‚’getNoteSummaryใ—ใŸใ‚‚ใฎใซ็ฝฎใๆ›ใˆใ‚‹ text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), - ...{ - cw: undefined, - reply: undefined, - renote: undefined, - user: undefined as any, // ้€š็Ÿฅใ‚’ๅ—ใ‘ๅ–ใฃใŸใƒฆใƒผใ‚ถใƒผใงใ‚ใ‚‹ๅ ดๅˆใŒๅคšใ„ใฎใงใ“ใ‚Œใ‚‚ๆจใฆใ‚‹ - } + + cw: undefined, + reply: undefined, + renote: undefined, + user: undefined as any, // ้€š็Ÿฅใ‚’ๅ—ใ‘ๅ–ใฃใŸใƒฆใƒผใ‚ถใƒผใงใ‚ใ‚‹ๅ ดๅˆใŒๅคšใ„ใฎใงใ“ใ‚Œใ‚‚ๆจใฆใ‚‹ } }; } @@ -30,7 +36,7 @@ function truncateNotification(notification: Packed<'Notification'>): any { return notification; } -export default async function(userId: string, type: notificationType, body: notificationBody) { +export async function pushNotification(userId: string, type: T, body: pushNotificationsTypes[T]) { const meta = await fetchMeta(); if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index 1ab45588da..6bc4304436 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -1,4 +1,4 @@ -import { createSystemUser } from './create-system-user.js'; +import { IsNull } from 'typeorm'; import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js'; import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; @@ -8,7 +8,7 @@ import { Users, Relays } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { Cache } from '@/misc/cache.js'; import { Relay } from '@/models/entities/relay.js'; -import { IsNull } from 'typeorm'; +import { createSystemUser } from './create-system-user.js'; const ACTOR_USERNAME = 'relay.actor' as const; @@ -88,6 +88,8 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act })); if (relays.length === 0) return; + // TODO + //const copy = structuredClone(activity); const copy = JSON.parse(JSON.stringify(activity)); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; diff --git a/packages/backend/test/.eslintrc b/packages/backend/test/.eslintrc deleted file mode 100644 index cea1b11388..0000000000 --- a/packages/backend/test/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "env": { - "node": true, - "mocha": true, - "commonjs": true - } -} diff --git a/packages/backend/test/.eslintrc.cjs b/packages/backend/test/.eslintrc.cjs new file mode 100644 index 0000000000..d83dc37d2f --- /dev/null +++ b/packages/backend/test/.eslintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + extends: ['../.eslintrc.cjs'], + env: { + node: true, + mocha: true, + }, +}; diff --git a/packages/backend/test/activitypub.ts b/packages/backend/test/activitypub.ts index 70f35cafd8..f4ae27e5ec 100644 --- a/packages/backend/test/activitypub.ts +++ b/packages/backend/test/activitypub.ts @@ -1,12 +1,14 @@ process.env.NODE_ENV = 'test'; -import rndstr from 'rndstr'; import * as assert from 'assert'; +import rndstr from 'rndstr'; +import { initDb } from '../src/db/postgre.js'; import { initTestDb } from './utils.js'; describe('ActivityPub', () => { before(async () => { - await initTestDb(); + //await initTestDb(); + await initDb(); }); describe('Parse minimum object', () => { @@ -57,8 +59,8 @@ describe('ActivityPub', () => { const note = await createNote(post.id, resolver, true); assert.deepStrictEqual(note?.uri, post.id); - assert.deepStrictEqual(note?.visibility, 'public'); - assert.deepStrictEqual(note?.text, post.content); + assert.deepStrictEqual(note.visibility, 'public'); + assert.deepStrictEqual(note.text, post.content); }); }); diff --git a/packages/backend/test/ap-request.ts b/packages/backend/test/ap-request.ts index 48f4fceb51..da95c421f3 100644 --- a/packages/backend/test/ap-request.ts +++ b/packages/backend/test/ap-request.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; +import httpSignature from 'http-signature'; import { genRsaKeyPair } from '../src/misc/gen-key-pair.js'; import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js'; -import httpSignature from 'http-signature'; export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { return { @@ -13,7 +13,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a signature: signature, }, signingString: signingString, - algorithm: algorithm?.toUpperCase(), + algorithm: algorithm.toUpperCase(), keyId: 'KeyID', // dummy, not used for verify }; }; @@ -26,7 +26,7 @@ describe('ap-request', () => { const activity = { a: 1 }; const body = JSON.stringify(activity); const headers = { - 'User-Agent': 'UA' + 'User-Agent': 'UA', }; const req = createSignedPost({ key, url, body, additionalHeaders: headers }); @@ -42,7 +42,7 @@ describe('ap-request', () => { const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; const url = 'https://example.com/outbox'; const headers = { - 'User-Agent': 'UA' + 'User-Agent': 'UA', }; const req = createSignedGet({ key, url, additionalHeaders: headers }); diff --git a/packages/backend/test/api-visibility.ts b/packages/backend/test/api-visibility.ts index d946191be8..b155549f98 100644 --- a/packages/backend/test/api-visibility.ts +++ b/packages/backend/test/api-visibility.ts @@ -61,40 +61,40 @@ describe('API visibility', () => { const show = async (noteId: any, by: any) => { return await request('/notes/show', { - noteId + noteId, }, by); }; before(async () => { //#region prepare // signup - alice = await signup({ username: 'alice' }); + alice = await signup({ username: 'alice' }); follower = await signup({ username: 'follower' }); - other = await signup({ username: 'other' }); - target = await signup({ username: 'target' }); - target2 = await signup({ username: 'target2' }); + other = await signup({ username: 'other' }); + target = await signup({ username: 'target' }); + target2 = await signup({ username: 'target2' }); // follow alice <= follower await request('/following/create', { userId: alice.id }, follower); // normal posts - pub = await post(alice, { text: 'x', visibility: 'public' }); + pub = await post(alice, { text: 'x', visibility: 'public' }); home = await post(alice, { text: 'x', visibility: 'home' }); - fol = await post(alice, { text: 'x', visibility: 'followers' }); - spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); + fol = await post(alice, { text: 'x', visibility: 'followers' }); + spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); // replies tgt = await post(target, { text: 'y', visibility: 'public' }); - pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); + pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); - folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); - speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); + folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); + speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); // mentions - pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); + pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); - folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); - speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); + folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); + speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); //#endregion }); diff --git a/packages/backend/test/block.ts b/packages/backend/test/block.ts index 103eec991d..b3343813cd 100644 --- a/packages/backend/test/block.ts +++ b/packages/backend/test/block.ts @@ -25,7 +25,7 @@ describe('Block', () => { it('Blockไฝœๆˆ', async(async () => { const res = await request('/blocking/create', { - userId: bob.id + userId: bob.id, }, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/chart.ts b/packages/backend/test/chart.ts index c8cea874f0..ac0844679f 100644 --- a/packages/backend/test/chart.ts +++ b/packages/backend/test/chart.ts @@ -2,30 +2,21 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as lolex from '@sinonjs/fake-timers'; -import { async, initTestDb } from './utils.js'; import TestChart from '../src/services/chart/charts/test.js'; import TestGroupedChart from '../src/services/chart/charts/test-grouped.js'; import TestUniqueChart from '../src/services/chart/charts/test-unique.js'; import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js'; -import * as _TestChart from '../src/services/chart/charts/entities/test.js'; -import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped.js'; -import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique.js'; -import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection.js'; +import { initDb } from '../src/db/postgre.js'; describe('Chart', () => { let testChart: TestChart; let testGroupedChart: TestGroupedChart; let testUniqueChart: TestUniqueChart; let testIntersectionChart: TestIntersectionChart; - let clock: lolex.Clock; + let clock: lolex.InstalledClock; - beforeEach(async(async () => { - await initTestDb(false, [ - _TestChart.entity.hour, _TestChart.entity.day, - _TestGroupedChart.entity.hour, _TestGroupedChart.entity.day, - _TestUniqueChart.entity.hour, _TestUniqueChart.entity.day, - _TestIntersectionChart.entity.hour, _TestIntersectionChart.entity.day, - ]); + beforeEach(async () => { + await initDb(true); testChart = new TestChart(); testGroupedChart = new TestGroupedChart(); @@ -33,15 +24,16 @@ describe('Chart', () => { testIntersectionChart = new TestIntersectionChart(); clock = lolex.install({ - now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)) + now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), + shouldClearNativeTimers: true, }); - })); + }); - afterEach(async(async () => { + afterEach(() => { clock.uninstall(); - })); + }); - it('Can updates', async(async () => { + it('Can updates', async () => { await testChart.increment(); await testChart.save(); @@ -52,7 +44,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -60,12 +52,12 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); - })); + }); - it('Can updates (dec)', async(async () => { + it('Can updates (dec)', async () => { await testChart.decrement(); await testChart.save(); @@ -76,7 +68,7 @@ describe('Chart', () => { foo: { dec: [1, 0, 0], inc: [0, 0, 0], - total: [-1, 0, 0] + total: [-1, 0, 0], }, }); @@ -84,12 +76,12 @@ describe('Chart', () => { foo: { dec: [1, 0, 0], inc: [0, 0, 0], - total: [-1, 0, 0] + total: [-1, 0, 0], }, }); - })); + }); - it('Empty chart', async(async () => { + it('Empty chart', async () => { const chartHours = await testChart.getChart('hour', 3, null); const chartDays = await testChart.getChart('day', 3, null); @@ -97,7 +89,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [0, 0, 0] + total: [0, 0, 0], }, }); @@ -105,12 +97,12 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [0, 0, 0] + total: [0, 0, 0], }, }); - })); + }); - it('Can updates at multiple times at same time', async(async () => { + it('Can updates at multiple times at same time', async () => { await testChart.increment(); await testChart.increment(); await testChart.increment(); @@ -123,7 +115,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [3, 0, 0], - total: [3, 0, 0] + total: [3, 0, 0], }, }); @@ -131,12 +123,12 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [3, 0, 0], - total: [3, 0, 0] + total: [3, 0, 0], }, }); - })); + }); - it('่ค‡ๆ•ฐๅ›žsaveใ•ใ‚Œใฆใ‚‚ใƒ‡ใƒผใ‚ฟใฎๆ›ดๆ–ฐใฏไธ€ๅบฆใ ใ‘', async(async () => { + it('่ค‡ๆ•ฐๅ›žsaveใ•ใ‚Œใฆใ‚‚ใƒ‡ใƒผใ‚ฟใฎๆ›ดๆ–ฐใฏไธ€ๅบฆใ ใ‘', async () => { await testChart.increment(); await testChart.save(); await testChart.save(); @@ -149,7 +141,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -157,12 +149,12 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); - })); + }); - it('Can updates at different times', async(async () => { + it('Can updates at different times', async () => { await testChart.increment(); await testChart.save(); @@ -178,7 +170,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 1, 0], - total: [2, 1, 0] + total: [2, 1, 0], }, }); @@ -186,14 +178,14 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); - })); + }); // ไป•ๆง˜ไธŠใฏใ“ใ†ใชใฃใฆใปใ—ใ„ใ‘ใฉใ€ๅฎŸ่ฃ…ใฏ้›ฃใ—ใใ†ใชใฎใงskip /* - it('Can updates at different times without save', async(async () => { + it('Can updates at different times without save', async () => { await testChart.increment(); clock.tick('01:00:00'); @@ -219,10 +211,10 @@ describe('Chart', () => { total: [2, 0, 0] }, }); - })); + }); */ - it('Can padding', async(async () => { + it('Can padding', async () => { await testChart.increment(); await testChart.save(); @@ -238,7 +230,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 1], - total: [2, 1, 1] + total: [2, 1, 1], }, }); @@ -246,13 +238,13 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); - })); + }); // ่ฆๆฑ‚ใ•ใ‚ŒใŸ็ฏ„ๅ›ฒใซใƒญใ‚ฐใŒใฒใจใคใ‚‚ใชใ„ๅ ดๅˆใงใ‚‚ใƒ‘ใƒ‡ใ‚ฃใƒณใ‚ฐใงใใ‚‹ - it('Can padding from past range', async(async () => { + it('Can padding from past range', async () => { await testChart.increment(); await testChart.save(); @@ -265,7 +257,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [1, 1, 1] + total: [1, 1, 1], }, }); @@ -273,14 +265,14 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); - })); + }); // ่ฆๆฑ‚ใ•ใ‚ŒใŸ็ฏ„ๅ›ฒใฎๆœ€ใ‚‚ๅคใ„็ฎ‡ๆ‰€ใซไฝ็ฝฎใ™ใ‚‹ใƒญใ‚ฐใŒๅญ˜ๅœจใ—ใชใ„ๅ ดๅˆใงใ‚‚ใƒ‘ใƒ‡ใ‚ฃใƒณใ‚ฐใงใใ‚‹ // Issue #3190 - it('Can padding from past range 2', async(async () => { + it('Can padding from past range 2', async () => { await testChart.increment(); await testChart.save(); @@ -296,7 +288,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [2, 1, 1] + total: [2, 1, 1], }, }); @@ -304,12 +296,12 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); - })); + }); - it('Can specify offset', async(async () => { + it('Can specify offset', async () => { await testChart.increment(); await testChart.save(); @@ -325,7 +317,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -333,12 +325,12 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); - })); + }); - it('Can specify offset (floor time)', async(async () => { + it('Can specify offset (floor time)', async () => { clock.tick('00:30:00'); await testChart.increment(); @@ -356,7 +348,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -364,13 +356,13 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); - })); + }); describe('Grouped', () => { - it('Can updates', async(async () => { + it('Can updates', async () => { await testGroupedChart.increment('alice'); await testGroupedChart.save(); @@ -383,7 +375,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -391,7 +383,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -399,7 +391,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [0, 0, 0] + total: [0, 0, 0], }, }); @@ -407,14 +399,14 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [0, 0, 0] + total: [0, 0, 0], }, }); - })); + }); }); describe('Unique increment', () => { - it('Can updates', async(async () => { + it('Can updates', async () => { await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('bob'); @@ -430,10 +422,10 @@ describe('Chart', () => { assert.deepStrictEqual(chartDays, { foo: [2, 0, 0], }); - })); + }); describe('Intersection', () => { - it('ๆกไปถใŒๆบ€ใŸใ•ใ‚Œใฆใ„ใชใ„ๅ ดๅˆใฏใ‚ซใ‚ฆใƒณใƒˆใ•ใ‚Œใชใ„', async(async () => { + it('ๆกไปถใŒๆบ€ใŸใ•ใ‚Œใฆใ„ใชใ„ๅ ดๅˆใฏใ‚ซใ‚ฆใƒณใƒˆใ•ใ‚Œใชใ„', async () => { await testIntersectionChart.addA('alice'); await testIntersectionChart.addA('bob'); await testIntersectionChart.addB('carol'); @@ -453,9 +445,9 @@ describe('Chart', () => { b: [1, 0, 0], aAndB: [0, 0, 0], }); - })); + }); - it('ๆกไปถใŒๆบ€ใŸใ•ใ‚Œใฆใ„ใ‚‹ๅ ดๅˆใซใ‚ซใ‚ฆใƒณใƒˆใ•ใ‚Œใ‚‹', async(async () => { + it('ๆกไปถใŒๆบ€ใŸใ•ใ‚Œใฆใ„ใ‚‹ๅ ดๅˆใซใ‚ซใ‚ฆใƒณใƒˆใ•ใ‚Œใ‚‹', async () => { await testIntersectionChart.addA('alice'); await testIntersectionChart.addA('bob'); await testIntersectionChart.addB('carol'); @@ -476,12 +468,12 @@ describe('Chart', () => { b: [2, 0, 0], aAndB: [1, 0, 0], }); - })); + }); }); }); describe('Resync', () => { - it('Can resync', async(async () => { + it('Can resync', async () => { testChart.total = 1; await testChart.resync(); @@ -493,7 +485,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -501,12 +493,12 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); - })); + }); - it('Can resync (2)', async(async () => { + it('Can resync (2)', async () => { await testChart.increment(); await testChart.save(); @@ -523,7 +515,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 1, 0], - total: [100, 1, 0] + total: [100, 1, 0], }, }); @@ -531,9 +523,9 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [100, 0, 0] + total: [100, 0, 0], }, }); - })); + }); }); }); diff --git a/packages/backend/test/extract-mentions.ts b/packages/backend/test/extract-mentions.ts index 9bfbc4192a..85afb098d8 100644 --- a/packages/backend/test/extract-mentions.ts +++ b/packages/backend/test/extract-mentions.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; -import { extractMentions } from '../src/misc/extract-mentions.js'; import { parse } from 'mfm-js'; +import { extractMentions } from '../src/misc/extract-mentions.js'; describe('Extract mentions', () => { it('simple', () => { @@ -10,15 +10,15 @@ describe('Extract mentions', () => { assert.deepStrictEqual(mentions, [{ username: 'foo', acct: '@foo', - host: null + host: null, }, { username: 'bar', acct: '@bar', - host: null + host: null, }, { username: 'baz', acct: '@baz', - host: null + host: null, }]); }); @@ -28,15 +28,15 @@ describe('Extract mentions', () => { assert.deepStrictEqual(mentions, [{ username: 'foo', acct: '@foo', - host: null + host: null, }, { username: 'bar', acct: '@bar', - host: null + host: null, }, { username: 'baz', acct: '@baz', - host: null + host: null, }]); }); }); diff --git a/packages/backend/test/fetch-resource.ts b/packages/backend/test/fetch-resource.ts index 4cb4b42562..ddb0e94b86 100644 --- a/packages/backend/test/fetch-resource.ts +++ b/packages/backend/test/fetch-resource.ts @@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js'; import * as openapi from '@redocly/openapi-core'; +import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js'; // Request Accept const ONLY_AP = 'application/activity+json'; @@ -26,7 +26,7 @@ describe('Fetch resource', () => { p = await startServer(); alice = await signup({ username: 'alice' }); alicesPost = await post(alice, { - text: 'test' + text: 'test', }); }); @@ -70,7 +70,7 @@ describe('Fetch resource', () => { const config = await openapi.loadConfig(); const result = await openapi.bundle({ config, - ref: `http://localhost:${port}/api.json` + ref: `http://localhost:${port}/api.json`, }); for (const problem of result.problems) { diff --git a/packages/backend/test/get-file-info.ts b/packages/backend/test/get-file-info.ts index 20061b8708..7ce98db50f 100644 --- a/packages/backend/test/get-file-info.ts +++ b/packages/backend/test/get-file-info.ts @@ -1,10 +1,15 @@ import * as assert from 'assert'; -import { async } from './utils.js'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; import { getFileInfo } from '../src/misc/get-file-info.js'; +import { async } from './utils.js'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); describe('Get file info', () => { it('Empty file', async (async () => { - const path = `${__dirname}/resources/emptyfile`; + const path = `${_dirname}/resources/emptyfile`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -13,7 +18,7 @@ describe('Get file info', () => { md5: 'd41d8cd98f00b204e9800998ecf8427e', type: { mime: 'application/octet-stream', - ext: null + ext: null, }, width: undefined, height: undefined, @@ -22,7 +27,7 @@ describe('Get file info', () => { })); it('Generic JPEG', async (async () => { - const path = `${__dirname}/resources/Lenna.jpg`; + const path = `${_dirname}/resources/Lenna.jpg`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -31,7 +36,7 @@ describe('Get file info', () => { md5: '091b3f259662aa31e2ffef4519951168', type: { mime: 'image/jpeg', - ext: 'jpg' + ext: 'jpg', }, width: 512, height: 512, @@ -40,7 +45,7 @@ describe('Get file info', () => { })); it('Generic APNG', async (async () => { - const path = `${__dirname}/resources/anime.png`; + const path = `${_dirname}/resources/anime.png`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -49,7 +54,7 @@ describe('Get file info', () => { md5: '08189c607bea3b952704676bb3c979e0', type: { mime: 'image/apng', - ext: 'apng' + ext: 'apng', }, width: 256, height: 256, @@ -58,7 +63,7 @@ describe('Get file info', () => { })); it('Generic AGIF', async (async () => { - const path = `${__dirname}/resources/anime.gif`; + const path = `${_dirname}/resources/anime.gif`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -67,7 +72,7 @@ describe('Get file info', () => { md5: '32c47a11555675d9267aee1a86571e7e', type: { mime: 'image/gif', - ext: 'gif' + ext: 'gif', }, width: 256, height: 256, @@ -76,7 +81,7 @@ describe('Get file info', () => { })); it('PNG with alpha', async (async () => { - const path = `${__dirname}/resources/with-alpha.png`; + const path = `${_dirname}/resources/with-alpha.png`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -85,7 +90,7 @@ describe('Get file info', () => { md5: 'f73535c3e1e27508885b69b10cf6e991', type: { mime: 'image/png', - ext: 'png' + ext: 'png', }, width: 256, height: 256, @@ -94,7 +99,7 @@ describe('Get file info', () => { })); it('Generic SVG', async (async () => { - const path = `${__dirname}/resources/image.svg`; + const path = `${_dirname}/resources/image.svg`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -103,7 +108,7 @@ describe('Get file info', () => { md5: 'b6f52b4b021e7b92cdd04509c7267965', type: { mime: 'image/svg+xml', - ext: 'svg' + ext: 'svg', }, width: 256, height: 256, @@ -113,7 +118,7 @@ describe('Get file info', () => { it('SVG with XML definition', async (async () => { // https://github.com/misskey-dev/misskey/issues/4413 - const path = `${__dirname}/resources/with-xml-def.svg`; + const path = `${_dirname}/resources/with-xml-def.svg`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -122,7 +127,7 @@ describe('Get file info', () => { md5: '4b7a346cde9ccbeb267e812567e33397', type: { mime: 'image/svg+xml', - ext: 'svg' + ext: 'svg', }, width: 256, height: 256, @@ -131,7 +136,7 @@ describe('Get file info', () => { })); it('Dimension limit', async (async () => { - const path = `${__dirname}/resources/25000x25000.png`; + const path = `${_dirname}/resources/25000x25000.png`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -140,7 +145,7 @@ describe('Get file info', () => { md5: '268c5dde99e17cf8fe09f1ab3f97df56', type: { mime: 'application/octet-stream', // do not treat as image - ext: null + ext: null, }, width: 25000, height: 25000, @@ -149,7 +154,7 @@ describe('Get file info', () => { })); it('Rotate JPEG', async (async () => { - const path = `${__dirname}/resources/rotate.jpg`; + const path = `${_dirname}/resources/rotate.jpg`; const info = await getFileInfo(path) as any; delete info.warnings; delete info.blurhash; @@ -158,7 +163,7 @@ describe('Get file info', () => { md5: '68d5b2d8d1d1acbbce99203e3ec3857e', type: { mime: 'image/jpeg', - ext: 'jpg' + ext: 'jpg', }, width: 512, height: 256, diff --git a/packages/backend/test/loader.js b/packages/backend/test/loader.js index 016f32f1a8..6b21587e32 100644 --- a/packages/backend/test/loader.js +++ b/packages/backend/test/loader.js @@ -1,37 +1,34 @@ -import path from 'path' -import typescript from 'typescript' -import { createMatchPath } from 'tsconfig-paths' -import { resolve as BaseResolve, getFormat, transformSource } from 'ts-node/esm' +/** + * ts-node/esmใƒญใƒผใƒ€ใƒผใซๆŠ•ใ’ใ‚‹ๅ‰ใซpath mappingใ‚’่งฃๆฑบใ™ใ‚‹ + * ๅ‚่€ƒ + * - https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115 + * - https://nodejs.org/api/esm.html#loaders + * โ€ป https://github.com/TypeStrong/ts-node/pull/1585 ใŒๅ–ใ‚Š่พผใพใ‚ŒใŸใ‚‰ใ“ใฎใ‚ซใ‚นใ‚ฟใƒ ใƒญใƒผใƒ€ใƒผใฏๅฟ…่ฆใชใใชใ‚‹ + */ -const { readConfigFile, parseJsonConfigFileContent, sys } = typescript +import { resolve as resolveTs, load } from 'ts-node/esm'; +import { loadConfig, createMatchPath } from 'tsconfig-paths'; +import { pathToFileURL } from 'url'; -const __dirname = path.dirname(new URL(import.meta.url).pathname) +const tsconfig = loadConfig(); +const matchPath = createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths); -const configFile = readConfigFile('./test/tsconfig.json', sys.readFile) -if (typeof configFile.error !== 'undefined') { - throw new Error(`Failed to load tsconfig: ${configFile.error}`) +export function resolve(specifier, ctx, defaultResolve) { + let resolvedSpecifier; + if (specifier.endsWith('.js')) { + // maybe transpiled + const specifierWithoutExtension = specifier.substring(0, specifier.length - '.js'.length); + const matchedSpecifier = matchPath(specifierWithoutExtension); + if (matchedSpecifier) { + resolvedSpecifier = pathToFileURL(`${matchedSpecifier}.js`).href; + } + } else { + const matchedSpecifier = matchPath(specifier); + if (matchedSpecifier) { + resolvedSpecifier = pathToFileURL(matchedSpecifier).href; + } + } + return resolveTs(resolvedSpecifier ?? specifier, ctx, defaultResolve); } -const { options } = parseJsonConfigFileContent( - configFile.config, - { - fileExists: sys.fileExists, - readFile: sys.readFile, - readDirectory: sys.readDirectory, - useCaseSensitiveFileNames: true, - }, - __dirname -) - -export { getFormat, transformSource } // ใ“ใ„ใคใ‚‰ใฏใใฎใพใพไฝฟใฃใฆใปใ—ใ„ใฎใง re-export ใ™ใ‚‹ - -const matchPath = createMatchPath(options.baseUrl, options.paths) - -export async function resolve(specifier, context, defaultResolve) { - const matchedSpecifier = matchPath(specifier.replace('.js', '.ts')) - return BaseResolve( // ts-node/esm ใฎ resolve ใซ tsconfig-paths ใง่งฃๆฑบใ—ใŸใƒ‘ใ‚นใ‚’ๆธกใ™ - matchedSpecifier ? `${matchedSpecifier}.ts` : specifier, - context, - defaultResolve - ) -} +export { load }; diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 5a46daf49f..ba89ac329a 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -11,7 +11,7 @@ export class MockResolver extends Resolver { public async _register(uri: string, content: string | Record, type = 'application/activity+json') { this._rs.set(uri, { type, - content: typeof content === 'string' ? content : JSON.stringify(content) + content: typeof content === 'string' ? content : JSON.stringify(content), }); } @@ -22,9 +22,9 @@ export class MockResolver extends Resolver { if (!r) { throw { - name: `StatusError`, + name: 'StatusError', statusCode: 404, - message: `Not registed for mock` + message: 'Not registed for mock', }; } diff --git a/packages/backend/test/mute.ts b/packages/backend/test/mute.ts index 288e8a8055..2be70f2b65 100644 --- a/packages/backend/test/mute.ts +++ b/packages/backend/test/mute.ts @@ -25,7 +25,7 @@ describe('Mute', () => { it('ใƒŸใƒฅใƒผใƒˆไฝœๆˆ', async(async () => { const res = await request('/mute/create', { - userId: carol.id + userId: carol.id, }, alice); assert.strictEqual(res.status, 204); @@ -117,7 +117,7 @@ describe('Mute', () => { const aliceNote = await post(alice); const carolNote = await post(carol); const bobNote = await post(bob, { - renoteId: carolNote.id + renoteId: carolNote.id, }); const res = await request('/notes/local-timeline', {}, alice); diff --git a/packages/backend/test/note.ts b/packages/backend/test/note.ts index 942b2709df..1183e9e4f1 100644 --- a/packages/backend/test/note.ts +++ b/packages/backend/test/note.ts @@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js'; import { Note } from '../src/models/entities/note.js'; +import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js'; describe('Note', () => { let p: childProcess.ChildProcess; @@ -26,7 +26,7 @@ describe('Note', () => { it('ๆŠ•็จฟใงใใ‚‹', async(async () => { const post = { - text: 'test' + text: 'test', }; const res = await request('/notes/create', post, alice); @@ -40,7 +40,7 @@ describe('Note', () => { const file = await uploadFile(alice); const res = await request('/notes/create', { - fileIds: [file.id] + fileIds: [file.id], }, alice); assert.strictEqual(res.status, 200); @@ -53,7 +53,7 @@ describe('Note', () => { const res = await request('/notes/create', { text: 'test', - fileIds: [file.id] + fileIds: [file.id], }, alice); assert.strictEqual(res.status, 200); @@ -64,7 +64,7 @@ describe('Note', () => { it('ๅญ˜ๅœจใ—ใชใ„ใƒ•ใ‚กใ‚คใƒซใฏ็„ก่ฆ–', async(async () => { const res = await request('/notes/create', { text: 'test', - fileIds: ['000000000000000000000000'] + fileIds: ['000000000000000000000000'], }, alice); assert.strictEqual(res.status, 200); @@ -74,19 +74,19 @@ describe('Note', () => { it('ไธๆญฃใชใƒ•ใ‚กใ‚คใƒซIDใงๆ€’ใ‚‰ใ‚Œใ‚‹', async(async () => { const res = await request('/notes/create', { - fileIds: ['kyoppie'] + fileIds: ['kyoppie'], }, alice); assert.strictEqual(res.status, 400); })); it('่ฟ”ไฟกใงใใ‚‹', async(async () => { const bobPost = await post(bob, { - text: 'foo' + text: 'foo', }); const alicePost = { text: 'bar', - replyId: bobPost.id + replyId: bobPost.id, }; const res = await request('/notes/create', alicePost, alice); @@ -100,11 +100,11 @@ describe('Note', () => { it('renoteใงใใ‚‹', async(async () => { const bobPost = await post(bob, { - text: 'test' + text: 'test', }); const alicePost = { - renoteId: bobPost.id + renoteId: bobPost.id, }; const res = await request('/notes/create', alicePost, alice); @@ -117,12 +117,12 @@ describe('Note', () => { it('ๅผ•็”จrenoteใงใใ‚‹', async(async () => { const bobPost = await post(bob, { - text: 'test' + text: 'test', }); const alicePost = { text: 'test', - renoteId: bobPost.id + renoteId: bobPost.id, }; const res = await request('/notes/create', alicePost, alice); @@ -136,7 +136,7 @@ describe('Note', () => { it('ๆ–‡ๅญ—ๆ•ฐใŽใ‚ŠใŽใ‚Šใงๆ€’ใ‚‰ใ‚Œใชใ„', async(async () => { const post = { - text: '!'.repeat(500) + text: '!'.repeat(500), }; const res = await request('/notes/create', post, alice); assert.strictEqual(res.status, 200); @@ -144,7 +144,7 @@ describe('Note', () => { it('ๆ–‡ๅญ—ๆ•ฐใ‚ชใƒผใƒใƒผใงๆ€’ใ‚‰ใ‚Œใ‚‹', async(async () => { const post = { - text: '!'.repeat(501) + text: '!'.repeat(501), }; const res = await request('/notes/create', post, alice); assert.strictEqual(res.status, 400); @@ -153,7 +153,7 @@ describe('Note', () => { it('ๅญ˜ๅœจใ—ใชใ„ใƒชใƒ—ใƒฉใ‚คๅ…ˆใงๆ€’ใ‚‰ใ‚Œใ‚‹', async(async () => { const post = { text: 'test', - replyId: '000000000000000000000000' + replyId: '000000000000000000000000', }; const res = await request('/notes/create', post, alice); assert.strictEqual(res.status, 400); @@ -161,7 +161,7 @@ describe('Note', () => { it('ๅญ˜ๅœจใ—ใชใ„renoteๅฏพ่ฑกใงๆ€’ใ‚‰ใ‚Œใ‚‹', async(async () => { const post = { - renoteId: '000000000000000000000000' + renoteId: '000000000000000000000000', }; const res = await request('/notes/create', post, alice); assert.strictEqual(res.status, 400); @@ -170,7 +170,7 @@ describe('Note', () => { it('ไธๆญฃใชใƒชใƒ—ใƒฉใ‚คๅ…ˆIDใงๆ€’ใ‚‰ใ‚Œใ‚‹', async(async () => { const post = { text: 'test', - replyId: 'foo' + replyId: 'foo', }; const res = await request('/notes/create', post, alice); assert.strictEqual(res.status, 400); @@ -178,7 +178,7 @@ describe('Note', () => { it('ไธๆญฃใชrenoteๅฏพ่ฑกIDใงๆ€’ใ‚‰ใ‚Œใ‚‹', async(async () => { const post = { - renoteId: 'foo' + renoteId: 'foo', }; const res = await request('/notes/create', post, alice); assert.strictEqual(res.status, 400); @@ -186,7 +186,7 @@ describe('Note', () => { it('ๅญ˜ๅœจใ—ใชใ„ใƒฆใƒผใ‚ถใƒผใซใƒกใƒณใ‚ทใƒงใƒณใงใใ‚‹', async(async () => { const post = { - text: '@ghost yo' + text: '@ghost yo', }; const res = await request('/notes/create', post, alice); @@ -198,7 +198,7 @@ describe('Note', () => { it('ๅŒใ˜ใƒฆใƒผใ‚ถใƒผใซ่ค‡ๆ•ฐใƒกใƒณใ‚ทใƒงใƒณใ—ใฆใ‚‚ๅ†…้ƒจ็š„ใซใพใจใ‚ใ‚‰ใ‚Œใ‚‹', async(async () => { const post = { - text: '@bob @bob @bob yo' + text: '@bob @bob @bob yo', }; const res = await request('/notes/create', post, alice); @@ -216,8 +216,8 @@ describe('Note', () => { const res = await request('/notes/create', { text: 'test', poll: { - choices: ['foo', 'bar'] - } + choices: ['foo', 'bar'], + }, }, alice); assert.strictEqual(res.status, 200); @@ -227,7 +227,7 @@ describe('Note', () => { it('ๆŠ•็ฅจใฎ้ธๆŠž่‚ขใŒ็„กใใฆๆ€’ใ‚‰ใ‚Œใ‚‹', async(async () => { const res = await request('/notes/create', { - poll: {} + poll: {}, }, alice); assert.strictEqual(res.status, 400); })); @@ -235,8 +235,8 @@ describe('Note', () => { it('ๆŠ•็ฅจใฎ้ธๆŠž่‚ขใŒ็„กใใฆๆ€’ใ‚‰ใ‚Œใ‚‹ (็ฉบใฎ้…ๅˆ—)', async(async () => { const res = await request('/notes/create', { poll: { - choices: [] - } + choices: [], + }, }, alice); assert.strictEqual(res.status, 400); })); @@ -244,8 +244,8 @@ describe('Note', () => { it('ๆŠ•็ฅจใฎ้ธๆŠž่‚ขใŒ1ใคใงๆ€’ใ‚‰ใ‚Œใ‚‹', async(async () => { const res = await request('/notes/create', { poll: { - choices: ['Strawberry Pasta'] - } + choices: ['Strawberry Pasta'], + }, }, alice); assert.strictEqual(res.status, 400); })); @@ -254,13 +254,13 @@ describe('Note', () => { const { body } = await request('/notes/create', { text: 'test', poll: { - choices: ['sakura', 'izumi', 'ako'] - } + choices: ['sakura', 'izumi', 'ako'], + }, }, alice); const res = await request('/notes/polls/vote', { noteId: body.createdNote.id, - choice: 1 + choice: 1, }, alice); assert.strictEqual(res.status, 204); @@ -270,18 +270,18 @@ describe('Note', () => { const { body } = await request('/notes/create', { text: 'test', poll: { - choices: ['sakura', 'izumi', 'ako'] - } + choices: ['sakura', 'izumi', 'ako'], + }, }, alice); await request('/notes/polls/vote', { noteId: body.createdNote.id, - choice: 0 + choice: 0, }, alice); const res = await request('/notes/polls/vote', { noteId: body.createdNote.id, - choice: 2 + choice: 2, }, alice); assert.strictEqual(res.status, 400); @@ -292,23 +292,23 @@ describe('Note', () => { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], - multiple: true - } + multiple: true, + }, }, alice); await request('/notes/polls/vote', { noteId: body.createdNote.id, - choice: 0 + choice: 0, }, alice); await request('/notes/polls/vote', { noteId: body.createdNote.id, - choice: 1 + choice: 1, }, alice); const res = await request('/notes/polls/vote', { noteId: body.createdNote.id, - choice: 2 + choice: 2, }, alice); assert.strictEqual(res.status, 204); @@ -319,15 +319,15 @@ describe('Note', () => { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], - expiredAfter: 1 - } + expiredAfter: 1, + }, }, alice); await new Promise(x => setTimeout(x, 2)); const res = await request('/notes/polls/vote', { noteId: body.createdNote.id, - choice: 1 + choice: 1, }, alice); assert.strictEqual(res.status, 400); @@ -341,11 +341,11 @@ describe('Note', () => { }, alice); const replyOneRes = await request('/notes/create', { text: 'reply one', - replyId: mainNoteRes.body.createdNote.id + replyId: mainNoteRes.body.createdNote.id, }, alice); const replyTwoRes = await request('/notes/create', { text: 'reply two', - replyId: mainNoteRes.body.createdNote.id + replyId: mainNoteRes.body.createdNote.id, }, alice); const deleteOneRes = await request('/notes/delete', { @@ -353,7 +353,7 @@ describe('Note', () => { }, alice); assert.strictEqual(deleteOneRes.status, 204); - let mainNote = await Notes.findOne({id: mainNoteRes.body.createdNote.id}); + let mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id }); assert.strictEqual(mainNote.repliesCount, 1); const deleteTwoRes = await request('/notes/delete', { @@ -361,7 +361,7 @@ describe('Note', () => { }, alice); assert.strictEqual(deleteTwoRes.status, 204); - mainNote = await Notes.findOne({id: mainNoteRes.body.createdNote.id}); + mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id }); assert.strictEqual(mainNote.repliesCount, 0); })); }); diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts index 84e43d26c2..df102c8dfe 100644 --- a/packages/backend/test/prelude/url.ts +++ b/packages/backend/test/prelude/url.ts @@ -6,7 +6,7 @@ describe('url', () => { const s = query({ foo: 'ใตใ…', bar: 'b a r', - baz: undefined + baz: undefined, }); assert.deepStrictEqual(s, 'foo=%E3%81%B5%E3%81%85&bar=b%20a%20r'); }); diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts index 8d22b6d3d3..f080b71dd4 100644 --- a/packages/backend/test/streaming.ts +++ b/packages/backend/test/streaming.ts @@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js'; import { Following } from '../src/models/entities/following.js'; +import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js'; describe('Streaming', () => { let p: childProcess.ChildProcess; @@ -30,7 +30,7 @@ describe('Streaming', () => { followerSharedInbox: null, followeeHost: followee.host, followeeInbox: null, - followeeSharedInbox: null + followeeSharedInbox: null, }); }; @@ -47,7 +47,7 @@ describe('Streaming', () => { }); post(alice, { - text: 'foo @bob bar' + text: 'foo @bob bar', }); })); @@ -55,7 +55,7 @@ describe('Streaming', () => { const alice = await signup({ username: 'alice' }); const bob = await signup({ username: 'bob' }); const bobNote = await post(bob, { - text: 'foo' + text: 'foo', }); const ws = await connectStream(bob, 'main', ({ type, body }) => { @@ -67,14 +67,14 @@ describe('Streaming', () => { }); post(alice, { - renoteId: bobNote.id + renoteId: bobNote.id, }); })); describe('Home Timeline', () => { it('่‡ชๅˆ†ใฎๆŠ•็จฟใŒๆตใ‚Œใ‚‹', () => new Promise(async done => { const post = { - text: 'foo' + text: 'foo', }; const me = await signup(); @@ -96,7 +96,7 @@ describe('Streaming', () => { // Alice ใŒ Bob ใ‚’ใƒ•ใ‚ฉใƒญใƒผ await request('/following/create', { - userId: bob.id + userId: bob.id, }, alice); const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { @@ -108,7 +108,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); })); @@ -125,7 +125,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); setTimeout(() => { @@ -141,7 +141,7 @@ describe('Streaming', () => { // Alice ใŒ Bob ใ‚’ใƒ•ใ‚ฉใƒญใƒผ await request('/following/create', { - userId: bob.id + userId: bob.id, }, alice); const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { @@ -157,7 +157,7 @@ describe('Streaming', () => { post(bob, { text: 'foo', visibility: 'specified', - visibleUserIds: [alice.id] + visibleUserIds: [alice.id], }); })); @@ -168,7 +168,7 @@ describe('Streaming', () => { // Alice ใŒ Bob ใ‚’ใƒ•ใ‚ฉใƒญใƒผ await request('/following/create', { - userId: bob.id + userId: bob.id, }, alice); let fired = false; @@ -183,7 +183,7 @@ describe('Streaming', () => { post(bob, { text: 'foo', visibility: 'specified', - visibleUserIds: [carol.id] + visibleUserIds: [carol.id], }); setTimeout(() => { @@ -207,7 +207,7 @@ describe('Streaming', () => { }); post(me, { - text: 'foo' + text: 'foo', }); })); @@ -224,7 +224,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); })); @@ -241,7 +241,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); setTimeout(() => { @@ -257,7 +257,7 @@ describe('Streaming', () => { // Alice ใŒ Bob ใ‚’ใƒ•ใ‚ฉใƒญใƒผ await request('/following/create', { - userId: bob.id + userId: bob.id, }, alice); let fired = false; @@ -269,7 +269,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); setTimeout(() => { @@ -294,7 +294,7 @@ describe('Streaming', () => { // ใƒ›ใƒผใƒ ๆŒ‡ๅฎš post(bob, { text: 'foo', - visibility: 'home' + visibility: 'home', }); setTimeout(() => { @@ -310,7 +310,7 @@ describe('Streaming', () => { // Alice ใŒ Bob ใ‚’ใƒ•ใ‚ฉใƒญใƒผ await request('/following/create', { - userId: bob.id + userId: bob.id, }, alice); let fired = false; @@ -325,7 +325,7 @@ describe('Streaming', () => { post(bob, { text: 'foo', visibility: 'specified', - visibleUserIds: [alice.id] + visibleUserIds: [alice.id], }); setTimeout(() => { @@ -350,7 +350,7 @@ describe('Streaming', () => { // ใƒ•ใ‚ฉใƒญใƒฏใƒผๅฎ›ใฆๆŠ•็จฟ post(bob, { text: 'foo', - visibility: 'followers' + visibility: 'followers', }); setTimeout(() => { @@ -374,7 +374,7 @@ describe('Streaming', () => { }); post(me, { - text: 'foo' + text: 'foo', }); })); @@ -391,7 +391,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); })); @@ -411,7 +411,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); })); @@ -428,7 +428,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); setTimeout(() => { @@ -444,7 +444,7 @@ describe('Streaming', () => { // Alice ใŒ Bob ใ‚’ใƒ•ใ‚ฉใƒญใƒผ await request('/following/create', { - userId: bob.id + userId: bob.id, }, alice); const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { @@ -460,7 +460,7 @@ describe('Streaming', () => { post(bob, { text: 'foo', visibility: 'specified', - visibleUserIds: [alice.id] + visibleUserIds: [alice.id], }); })); @@ -470,7 +470,7 @@ describe('Streaming', () => { // Alice ใŒ Bob ใ‚’ใƒ•ใ‚ฉใƒญใƒผ await request('/following/create', { - userId: bob.id + userId: bob.id, }, alice); const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { @@ -485,7 +485,7 @@ describe('Streaming', () => { // ใƒ›ใƒผใƒ ๆŠ•็จฟ post(bob, { text: 'foo', - visibility: 'home' + visibility: 'home', }); })); @@ -504,7 +504,7 @@ describe('Streaming', () => { // ใƒ›ใƒผใƒ ๆŠ•็จฟ post(bob, { text: 'foo', - visibility: 'home' + visibility: 'home', }); setTimeout(() => { @@ -529,7 +529,7 @@ describe('Streaming', () => { // ใƒ•ใ‚ฉใƒญใƒฏใƒผๅฎ›ใฆๆŠ•็จฟ post(bob, { text: 'foo', - visibility: 'followers' + visibility: 'followers', }); setTimeout(() => { @@ -554,7 +554,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); })); @@ -571,7 +571,7 @@ describe('Streaming', () => { }); post(bob, { - text: 'foo' + text: 'foo', }); })); @@ -590,7 +590,7 @@ describe('Streaming', () => { // ใƒ›ใƒผใƒ ๆŠ•็จฟ post(bob, { text: 'foo', - visibility: 'home' + visibility: 'home', }); setTimeout(() => { @@ -608,13 +608,13 @@ describe('Streaming', () => { // ใƒชใ‚นใƒˆไฝœๆˆ const list = await request('/users/lists/create', { - name: 'my list' + name: 'my list', }, alice).then(x => x.body); // Alice ใŒ Bob ใ‚’ใƒชใ‚นใ‚คใƒณ await request('/users/lists/push', { listId: list.id, - userId: bob.id + userId: bob.id, }, alice); const ws = await connectStream(alice, 'userList', ({ type, body }) => { @@ -624,11 +624,11 @@ describe('Streaming', () => { done(); } }, { - listId: list.id + listId: list.id, }); post(bob, { - text: 'foo' + text: 'foo', }); })); @@ -638,7 +638,7 @@ describe('Streaming', () => { // ใƒชใ‚นใƒˆไฝœๆˆ const list = await request('/users/lists/create', { - name: 'my list' + name: 'my list', }, alice).then(x => x.body); let fired = false; @@ -648,11 +648,11 @@ describe('Streaming', () => { fired = true; } }, { - listId: list.id + listId: list.id, }); post(bob, { - text: 'foo' + text: 'foo', }); setTimeout(() => { @@ -669,13 +669,13 @@ describe('Streaming', () => { // ใƒชใ‚นใƒˆไฝœๆˆ const list = await request('/users/lists/create', { - name: 'my list' + name: 'my list', }, alice).then(x => x.body); // Alice ใŒ Bob ใ‚’ใƒชใ‚นใ‚คใƒณ await request('/users/lists/push', { listId: list.id, - userId: bob.id + userId: bob.id, }, alice); const ws = await connectStream(alice, 'userList', ({ type, body }) => { @@ -686,14 +686,14 @@ describe('Streaming', () => { done(); } }, { - listId: list.id + listId: list.id, }); // Bob ใŒ Alice ๅฎ›ใฆใฎใƒ€ใ‚คใƒฌใ‚ฏใƒˆๆŠ•็จฟ post(bob, { text: 'foo', visibility: 'specified', - visibleUserIds: [alice.id] + visibleUserIds: [alice.id], }); })); @@ -704,13 +704,13 @@ describe('Streaming', () => { // ใƒชใ‚นใƒˆไฝœๆˆ const list = await request('/users/lists/create', { - name: 'my list' + name: 'my list', }, alice).then(x => x.body); // Alice ใŒ Bob ใ‚’ใƒชใ‚นใ‚คใƒณ await request('/users/lists/push', { listId: list.id, - userId: bob.id + userId: bob.id, }, alice); let fired = false; @@ -720,13 +720,13 @@ describe('Streaming', () => { fired = true; } }, { - listId: list.id + listId: list.id, }); // ใƒ•ใ‚ฉใƒญใƒฏใƒผๅฎ›ใฆๆŠ•็จฟ post(bob, { text: 'foo', - visibility: 'followers' + visibility: 'followers', }); setTimeout(() => { @@ -749,12 +749,12 @@ describe('Streaming', () => { } }, { q: [ - ['foo'] - ] + ['foo'], + ], }); post(me, { - text: '#foo' + text: '#foo', }); })); @@ -773,20 +773,20 @@ describe('Streaming', () => { } }, { q: [ - ['foo', 'bar'] - ] + ['foo', 'bar'], + ], }); post(me, { - text: '#foo' + text: '#foo', }); post(me, { - text: '#bar' + text: '#bar', }); post(me, { - text: '#foo #bar' + text: '#foo #bar', }); setTimeout(() => { @@ -816,24 +816,24 @@ describe('Streaming', () => { }, { q: [ ['foo'], - ['bar'] - ] + ['bar'], + ], }); post(me, { - text: '#foo' + text: '#foo', }); post(me, { - text: '#bar' + text: '#bar', }); post(me, { - text: '#foo #bar' + text: '#foo #bar', }); post(me, { - text: '#piyo' + text: '#piyo', }); setTimeout(() => { @@ -866,28 +866,28 @@ describe('Streaming', () => { }, { q: [ ['foo', 'bar'], - ['piyo'] - ] + ['piyo'], + ], }); post(me, { - text: '#foo' + text: '#foo', }); post(me, { - text: '#bar' + text: '#bar', }); post(me, { - text: '#foo #bar' + text: '#foo #bar', }); post(me, { - text: '#piyo' + text: '#piyo', }); post(me, { - text: '#waaa' + text: '#waaa', }); setTimeout(() => { diff --git a/packages/backend/test/user-notes.ts b/packages/backend/test/user-notes.ts index 25ffe04756..5b7933da67 100644 --- a/packages/backend/test/user-notes.ts +++ b/packages/backend/test/user-notes.ts @@ -2,8 +2,13 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { async, signup, request, post, uploadFile, startServer, shutdownServer } from './utils.js'; +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + describe('users/notes', () => { let p: childProcess.ChildProcess; @@ -15,16 +20,16 @@ describe('users/notes', () => { before(async () => { p = await startServer(); alice = await signup({ username: 'alice' }); - const jpg = await uploadFile(alice, __dirname + '/resources/Lenna.jpg'); - const png = await uploadFile(alice, __dirname + '/resources/Lenna.png'); + const jpg = await uploadFile(alice, _dirname + '/resources/Lenna.jpg'); + const png = await uploadFile(alice, _dirname + '/resources/Lenna.png'); jpgNote = await post(alice, { - fileIds: [jpg.id] + fileIds: [jpg.id], }); pngNote = await post(alice, { - fileIds: [png.id] + fileIds: [png.id], }); jpgPngNote = await post(alice, { - fileIds: [jpg.id, png.id] + fileIds: [jpg.id, png.id], }); }); @@ -35,7 +40,7 @@ describe('users/notes', () => { it('ใƒ•ใ‚กใ‚คใƒซใ‚ฟใ‚คใƒ—ๆŒ‡ๅฎš (jpg)', async(async () => { const res = await request('/users/notes', { userId: alice.id, - fileType: ['image/jpeg'] + fileType: ['image/jpeg'], }, alice); assert.strictEqual(res.status, 200); @@ -48,7 +53,7 @@ describe('users/notes', () => { it('ใƒ•ใ‚กใ‚คใƒซใ‚ฟใ‚คใƒ—ๆŒ‡ๅฎš (jpg or png)', async(async () => { const res = await request('/users/notes', { userId: alice.id, - fileType: ['image/jpeg', 'image/png'] + fileType: ['image/jpeg', 'image/png'], }, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 32a030f933..5eb4ed3b01 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,14 +1,20 @@ import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as childProcess from 'child_process'; +import * as http from 'node:http'; +import { SIGKILL } from 'constants'; import * as WebSocket from 'ws'; import * as misskey from 'misskey-js'; import fetch from 'node-fetch'; import FormData from 'form-data'; -import * as childProcess from 'child_process'; -import * as http from 'http'; +import { DataSource } from 'typeorm'; import loadConfig from '../src/config/load.js'; -import { SIGKILL } from 'constants'; import { entities } from '../src/db/postgre.js'; +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + const config = loadConfig(); export const port = config.port; @@ -22,29 +28,29 @@ export const async = (fn: Function) => (done: Function) => { export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => { const auth = me ? { - i: me.token + i: me.token, } : {}; const res = await fetch(`http://localhost:${port}/api${endpoint}`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, - body: JSON.stringify(Object.assign(auth, params)) + body: JSON.stringify(Object.assign(auth, params)), }); const status = res.status; const body = res.status !== 204 ? await res.json().catch() : null; return { - body, status + body, status, }; }; export const signup = async (params?: any): Promise => { const q = Object.assign({ username: 'test', - password: 'test' + password: 'test', }, params); const res = await request('/signup', q); @@ -54,7 +60,7 @@ export const signup = async (params?: any): Promise => { export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise => { const q = Object.assign({ - text: 'test' + text: 'test', }, params); const res = await request('/notes/create', q, user); @@ -65,26 +71,26 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create'] export const react = async (user: any, note: any, reaction: string): Promise => { await request('/notes/reactions/create', { noteId: note.id, - reaction: reaction + reaction: reaction, }, user); }; export const uploadFile = (user: any, path?: string): Promise => { - const formData = new FormData(); - formData.append('i', user.token); - formData.append('file', fs.createReadStream(path || __dirname + '/resources/Lenna.png')); + const formData = new FormData(); + formData.append('i', user.token); + formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png')); - return fetch(`http://localhost:${port}/api/drive/files/create`, { - method: 'post', - body: formData, - timeout: 30 * 1000, - }).then(res => { - if (!res.ok) { - throw `${res.status} ${res.statusText}`; - } else { - return res.json(); - } - }); + return fetch(`http://localhost:${port}/api/drive/files/create`, { + method: 'post', + body: formData, + timeout: 30 * 1000, + }).then(res => { + if (!res.ok) { + throw `${res.status} ${res.statusText}`; + } else { + return res.json(); + } + }); }; export function connectStream(user: any, channel: string, listener: (message: Record) => any, params?: any): Promise { @@ -94,9 +100,9 @@ export function connectStream(user: any, channel: string, listener: (message: Re ws.on('open', () => { ws.on('message', data => { const msg = JSON.parse(data.toString()); - if (msg.type == 'channel' && msg.body.id == 'a') { + if (msg.type === 'channel' && msg.body.id === 'a') { listener(msg.body); - } else if (msg.type == 'connected' && msg.body.id == 'a') { + } else if (msg.type === 'connected' && msg.body.id === 'a') { res(ws); } }); @@ -107,8 +113,8 @@ export function connectStream(user: any, channel: string, listener: (message: Re channel: channel, id: 'a', pong: true, - params: params - } + params: params, + }, })); }); }); @@ -119,8 +125,8 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status? return await new Promise((resolve, reject) => { const req = http.request(`http://localhost:${port}${path}`, { headers: { - Accept: accept - } + Accept: accept, + }, }, res => { if (res.statusCode! >= 400) { reject(res); @@ -139,9 +145,9 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status? export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise = async () => {}) { return (done: (err?: Error) => any) => { - const p = childProcess.spawn('node', [__dirname + '/../index.js'], { + const p = childProcess.spawn('node', [_dirname + '/../index.js'], { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], - env: { NODE_ENV: 'test', PATH: process.env.PATH } + env: { NODE_ENV: 'test', PATH: process.env.PATH }, }); callbackSpawnedProcess(p); p.on('message', message => { @@ -153,12 +159,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce export async function initTestDb(justBorrow = false, initEntities?: any[]) { if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; - try { - const conn = await getConnection(); - await conn.close(); - } catch (e) {} - - return await createConnection({ + const db = new DataSource({ type: 'postgres', host: config.db.host, port: config.db.port, @@ -167,8 +168,12 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { database: config.db.db, synchronize: true && !justBorrow, dropSchema: true && !justBorrow, - entities: initEntities || entities + entities: initEntities || entities, }); + + await db.initialize(); + + return db; } export function startServer(timeout = 30 * 1000): Promise { @@ -178,9 +183,9 @@ export function startServer(timeout = 30 * 1000): Promise rej(e)); diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 3120851aae..22338a4976 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -25,9 +25,14 @@ "rootDir": "./src", "baseUrl": "./", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] }, "outDir": "./built", + "types": [ + "node" + ], "typeRoots": [ "./node_modules/@types", "./src/@types" diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 5cd71acf9c..5ce87c34eb 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -12,20 +12,6 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99" integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw== -"@babel/runtime@^7.16.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" - integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.6.2": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" - integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/types@^7.6.1", "@babel/types@^7.9.6": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" @@ -35,68 +21,63 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@bull-board/api@3.10.3": - version "3.10.3" - resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.10.3.tgz#c6aad9f5cfb3acbe02c57e823ee81c1ae575849d" - integrity sha512-kV6EPwi9j71qBmozvDmtT01j986r4cFqNmBgq7HApYXW0G2U8Brmv0Ut0iMQZRc/X7aA5KYL3qXcEsriFnq+jw== +"@bull-board/api@3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.11.1.tgz#98b2c9556f643718bb5bde4a1306e6706af8192e" + integrity sha512-ElwX7sM+Ng4ZL9KUsbDubRE+r2hu/gss85OsROeE9bmyfkW14jOJkgr5MKUyjTTgPEeMs1Mw55TgQs2vxoWBiA== dependencies: redis-info "^3.0.8" -"@bull-board/koa@3.10.3": - version "3.10.3" - resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-3.10.3.tgz#b9f02629f96f056d6a038c3c58fc339d58e55abb" - integrity sha512-DK8m09MwcRwUR3tz3xI0iSK/Ih2huQ2MAWm8krYjO5deswP2yBaCWE4OtpiULLfVpf8z4zB3Oqa0xNJrKRHTOQ== +"@bull-board/koa@3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-3.11.1.tgz#1872aba2c65d116d1183b3003e4a2cb2c1e2fbbf" + integrity sha512-F/thrTuC1JWpdBO7DPdKD/wr8c+d7MJGu0sr5ARsT1WXhng7sU7OqBEP/5Y7HhByurjDFXDxcgk/mc78Tmeb/Q== dependencies: - "@bull-board/api" "3.10.3" - "@bull-board/ui" "3.10.3" - ejs "^3.1.6" + "@bull-board/api" "3.11.1" + "@bull-board/ui" "3.11.1" + ejs "^3.1.7" koa "^2.13.1" koa-mount "^4.0.0" koa-router "^10.0.0" koa-static "^5.0.0" koa-views "^7.0.1" -"@bull-board/ui@3.10.3": - version "3.10.3" - resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-3.10.3.tgz#b921199d42b32d8ddd9bbf0e35c25be0d64403e9" - integrity sha512-6zYW3FqySg+4IKEeM1jt/5ixNVBKQjtZLG9W81ADVcHk8YceQ++7URWzDb8nQEct3rEW4bjR6nicVWNXMSN7Lw== +"@bull-board/ui@3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-3.11.1.tgz#17a2af5573f31811a543105b9a96249c95e93ce7" + integrity sha512-SRrfvxHF/WaBICiAFuWAoAlTvoBYUBmX94oRbSKzVILRFZMe3gs0hN071BFohrn4yOTFHAkWPN7cjMbaqHwCag== dependencies: - "@bull-board/api" "3.10.3" + "@bull-board/api" "3.11.1" -"@cspotcode/source-map-consumer@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" - integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== - -"@cspotcode/source-map-support@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" - integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== dependencies: - "@cspotcode/source-map-consumer" "0.8.0" + "@jridgewell/trace-mapping" "0.3.9" "@cto.af/textdecoder@^0.0.0": version "0.0.0" resolved "https://registry.yarnpkg.com/@cto.af/textdecoder/-/textdecoder-0.0.0.tgz#e1e8d84c936c30a0f4619971f19ca41941af9fdc" integrity sha512-sJpx3F5xcVV/9jNYJQtvimo4Vfld/nD3ph+ZWtQzZ03Zo8rJC7QKQTRcIGS13Rcz80DwFNthCWMrd58vpY4ZAQ== -"@digitalbazaar/http-client@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-1.1.0.tgz#cac383b24ace04b18b919deab773462b03d3d7b0" - integrity sha512-ks7hqa6hm9NyULdbm9qL6TRS8rADzBw8R0lETvUgvdNXu9H62XG2YqoKRDThtfgWzWxLwRJ3Z2o4ev81dZZbyQ== +"@digitalbazaar/http-client@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-3.2.0.tgz#b85ea09028c7d0f288f976c852d0a8f3875f0fcf" + integrity sha512-NhYXcWE/JDE7AnJikNX7q0S6zNuUPA2NuIoRdUpmvHlarjmRqyr6hIO3Awu2FxlUzbdiI1uzuWrZyB9mD1tTvw== dependencies: - esm "^3.2.22" - ky "^0.25.1" - ky-universal "^0.8.2" + ky "^0.30.0" + ky-universal "^0.10.1" + undici "^5.2.0" -"@discordapp/twemoji@13.1.1": - version "13.1.1" - resolved "https://registry.yarnpkg.com/@discordapp/twemoji/-/twemoji-13.1.1.tgz#f750d491ffb740eca619fac0c63650c1de7fff91" - integrity sha512-WDnPjWq/trfCcZk7dzQ2cYH5v5XaIfPzyixJ//O9XKilYYZRVS3p61vFvax5qMwanMMbnNG1iOzeqHKtivO32A== +"@discordapp/twemoji@14.0.2": + version "14.0.2" + resolved "https://registry.yarnpkg.com/@discordapp/twemoji/-/twemoji-14.0.2.tgz#50cc19f6f3769dc6b36eb251421b5f5d4629e837" + integrity sha512-eYJpFsjViDTYwq3f6v+tRu8iRc+yLAeGrlh6kmNRvvC6rroUE2bMlBfEQ/WNh+2Q1FtSEFXpxzuQPOHzRzbAyA== dependencies: fs-extra "^8.0.1" jsonfile "^5.0.0" - twemoji-parser "13.1.0" + twemoji-parser "14.0.0" universalify "^0.1.2" "@elastic/elasticsearch@7.11.0": @@ -110,19 +91,19 @@ pump "^3.0.0" secure-json-parse "^2.1.0" -"@eslint/eslintrc@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" - integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== +"@eslint/eslintrc@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" + integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.3.1" - globals "^13.9.0" + espree "^9.3.2" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" - minimatch "^3.0.4" + minimatch "^3.1.2" strip-json-comments "^3.1.1" "@gar/promisify@^1.0.1": @@ -144,6 +125,24 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" + integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.13" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" + integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@koa/cors@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.1.0.tgz#618bb073438cfdbd3ebd0e648a76e33b84f3a3b2" @@ -234,6 +233,15 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@peertube/http-signature@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@peertube/http-signature/-/http-signature-1.6.0.tgz#22bef028384e6437e8dbd94052ba7b8bd7f7f1ae" + integrity sha512-Bx780c7FPYtkV4LgCoaJcXYcKQqaMef2iQR2V2r5klkYkIQWFxbTOpyhKxvVXYIBIFpj5Cb8DGVDAmhkm7aavg== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.14.1" + "@redocly/ajv@^8.6.4": version "8.6.4" resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.4.tgz#94053e7a9d4146d1a4feacd3813892873f229a85" @@ -244,10 +252,10 @@ require-from-string "^2.0.2" uri-js "^4.2.2" -"@redocly/openapi-core@1.0.0-beta.93": - version "1.0.0-beta.93" - resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.93.tgz#882db8684598217f621adc7349288229253a0038" - integrity sha512-xQj7UnjPj3mKtkyRrm+bjzEoyo0CVNjGP4pV6BzQ0vgKf0Jqq7apFC703psyBH+JscYr7NKK1hPQU76ylhFDdg== +"@redocly/openapi-core@1.0.0-beta.97": + version "1.0.0-beta.97" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.97.tgz#324ed46e9a9aee4c615be22ee348c53f7bb5f180" + integrity sha512-3WW9/6flosJuRtU3GI0Vw39OYFZqqXMDCp5TLa3EjXOb7Nm6AZTWRb3Y+I/+UdNJ/NTszVJkQczoa1t476ekiQ== dependencies: "@redocly/ajv" "^8.6.4" "@types/node" "^14.11.8" @@ -255,7 +263,7 @@ js-levenshtein "^1.1.6" js-yaml "^4.1.0" lodash.isequal "^4.5.0" - minimatch "^3.0.4" + minimatch "^5.0.1" node-fetch "^2.6.1" pluralize "^8.0.0" yaml-ast-parser "0.0.43" @@ -277,10 +285,10 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.1.tgz#7b698e0b9d12d93611f06ee143c30ced848e2840" - integrity sha512-Wp5vwlZ0lOqpSYGKqr53INws9HLkt6JDc/pDZcPf7bchQnrXJMXPns8CXx0hFikMSGSWfvtvvpb2gtMVfkWagA== +"@sinonjs/fake-timers@9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" + integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== dependencies: "@sinonjs/commons" "^1.7.0" @@ -527,6 +535,11 @@ resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.6.tgz#4396c0b17128abf5773bb68b5453b88fc565b0d4" integrity sha512-OUcfMjRie5IOrJulUQwVNvV57SOdKcTfBj3pjXNxzXqeOIrY2aGDNGW/Tlp83EQPkz4tCE6YWVrGuc/ZeaAQGg== +"@types/jsrsasign@10.5.1": + version "10.5.1" + resolved "https://registry.yarnpkg.com/@types/jsrsasign/-/jsrsasign-10.5.1.tgz#6f9defd46dfcf324b1cff08a06be639858deee3b" + integrity sha512-QqM03IXHY6SX835mWdx7Vp8ZOxw/hcnMjGjapUQf+pgFPRyGdjg3jxFsr4p+rolKcdRhptm3mtVQNk4OMhCQcA== + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -649,10 +662,10 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== -"@types/mocha@9.1.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5" - integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg== +"@types/mocha@9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== "@types/node-fetch@3.0.3": version "3.0.3" @@ -666,10 +679,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50" integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA== -"@types/node@17.0.23": - version "17.0.23" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" - integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== +"@types/node@17.0.41": + version "17.0.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b" + integrity sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw== "@types/node@^14.11.8": version "14.17.9" @@ -700,11 +713,6 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== -"@types/portscanner@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@types/portscanner/-/portscanner-2.1.1.tgz#89d5094e16f3d941f20f3889dfa5d3a164b3dd3b" - integrity sha512-1NsVIbgBKvrqxwtMN0V6CLji1ERwKSI/RWz0J3y++CzSwYNGBStCfpIFgxV3ZwxsDR5PoZqoUWhwraDm+Ztn0Q== - "@types/pug@2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6" @@ -777,6 +785,11 @@ dependencies: htmlparser2 "^6.0.0" +"@types/semver@7.3.9": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== + "@types/serve-static@*": version "1.13.3" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -785,10 +798,10 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -"@types/sharp@0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.1.tgz#31bd128f2437e8fc31424eb23d8284aa127bfa8d" - integrity sha512-LxzQsKo2YtvA2DlqACNXmlbLGMVJCSU/HhV4N9RrStClUEf02iN+AakD/zUOpZkbo1OG+lHk2LeqoHedLwln2w== +"@types/sharp@0.30.2": + version "0.30.2" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.2.tgz#df5ff34140b3bad165482e6f3d26b08e42a0503a" + integrity sha512-uLCBwjDg/BTcQit0dpNGvkIjvH3wsb8zpaJePCjvONBBSfaKHoxXBIuq1MT8DMQEfk2fKYnpC9QExCgFhkGkMQ== dependencies: "@types/node" "*" @@ -845,85 +858,85 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz#950df411cec65f90d75d6320a03b2c98f6c3af7d" - integrity sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A== +"@typescript-eslint/eslint-plugin@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.1.tgz#fdf59c905354139046b41b3ed95d1609913d0758" + integrity sha512-6dM5NKT57ZduNnJfpY81Phe9nc9wolnMCnknb1im6brWi1RYv84nbMS3olJa27B6+irUVV1X/Wb+Am0FjJdGFw== dependencies: - "@typescript-eslint/scope-manager" "5.18.0" - "@typescript-eslint/type-utils" "5.18.0" - "@typescript-eslint/utils" "5.18.0" - debug "^4.3.2" + "@typescript-eslint/scope-manager" "5.27.1" + "@typescript-eslint/type-utils" "5.27.1" + "@typescript-eslint/utils" "5.27.1" + debug "^4.3.4" functional-red-black-tree "^1.0.1" - ignore "^5.1.8" + ignore "^5.2.0" regexpp "^3.2.0" - semver "^7.3.5" + semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.18.0.tgz#2bcd4ff21df33621df33e942ccb21cb897f004c6" - integrity sha512-+08nYfurBzSSPndngnHvFw/fniWYJ5ymOrn/63oMIbgomVQOvIDhBoJmYZ9lwQOCnQV9xHGvf88ze3jFGUYooQ== +"@typescript-eslint/parser@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.27.1.tgz#3a4dcaa67e45e0427b6ca7bb7165122c8b569639" + integrity sha512-7Va2ZOkHi5NP+AZwb5ReLgNF6nWLGTeUJfxdkVUAPPSaAdbWNnFZzLZ4EGGmmiCTg+AwlbE1KyUYTBglosSLHQ== dependencies: - "@typescript-eslint/scope-manager" "5.18.0" - "@typescript-eslint/types" "5.18.0" - "@typescript-eslint/typescript-estree" "5.18.0" - debug "^4.3.2" + "@typescript-eslint/scope-manager" "5.27.1" + "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/typescript-estree" "5.27.1" + debug "^4.3.4" -"@typescript-eslint/scope-manager@5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.18.0.tgz#a7d7b49b973ba8cebf2a3710eefd457ef2fb5505" - integrity sha512-C0CZML6NyRDj+ZbMqh9FnPscg2PrzSaVQg3IpTmpe0NURMVBXlghGZgMYqBw07YW73i0MCqSDqv2SbywnCS8jQ== +"@typescript-eslint/scope-manager@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.1.tgz#4d1504392d01fe5f76f4a5825991ec78b7b7894d" + integrity sha512-fQEOSa/QroWE6fAEg+bJxtRZJTH8NTskggybogHt4H9Da8zd4cJji76gA5SBlR0MgtwF7rebxTbDKB49YUCpAg== dependencies: - "@typescript-eslint/types" "5.18.0" - "@typescript-eslint/visitor-keys" "5.18.0" + "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/visitor-keys" "5.27.1" -"@typescript-eslint/type-utils@5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74" - integrity sha512-vcn9/6J5D6jtHxpEJrgK8FhaM8r6J1/ZiNu70ZUJN554Y3D9t3iovi6u7JF8l/e7FcBIxeuTEidZDR70UuCIfA== +"@typescript-eslint/type-utils@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.27.1.tgz#369f695199f74c1876e395ebea202582eb1d4166" + integrity sha512-+UC1vVUWaDHRnC2cQrCJ4QtVjpjjCgjNFpg8b03nERmkHv9JV9X5M19D7UFMd+/G7T/sgFwX2pGmWK38rqyvXw== dependencies: - "@typescript-eslint/utils" "5.18.0" - debug "^4.3.2" + "@typescript-eslint/utils" "5.27.1" + debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e" - integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw== +"@typescript-eslint/types@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.1.tgz#34e3e629501349d38be6ae97841298c03a6ffbf1" + integrity sha512-LgogNVkBhCTZU/m8XgEYIWICD6m4dmEDbKXESCbqOXfKZxRKeqpiJXQIErv66sdopRKZPo5l32ymNqibYEH/xg== -"@typescript-eslint/typescript-estree@5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474" - integrity sha512-wa+2VAhOPpZs1bVij9e5gyVu60ReMi/KuOx4LKjGx2Y3XTNUDJgQ+5f77D49pHtqef/klglf+mibuHs9TrPxdQ== +"@typescript-eslint/typescript-estree@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.1.tgz#7621ee78607331821c16fffc21fc7a452d7bc808" + integrity sha512-DnZvvq3TAJ5ke+hk0LklvxwYsnXpRdqUY5gaVS0D4raKtbznPz71UJGnPTHEFo0GDxqLOLdMkkmVZjSpET1hFw== dependencies: - "@typescript-eslint/types" "5.18.0" - "@typescript-eslint/visitor-keys" "5.18.0" - debug "^4.3.2" - globby "^11.0.4" + "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/visitor-keys" "5.27.1" + debug "^4.3.4" + globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.5" + semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855" - integrity sha512-+hFGWUMMri7OFY26TsOlGa+zgjEy1ssEipxpLjtl4wSll8zy85x0GrUSju/FHdKfVorZPYJLkF3I4XPtnCTewA== +"@typescript-eslint/utils@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.27.1.tgz#b4678b68a94bc3b85bf08f243812a6868ac5128f" + integrity sha512-mZ9WEn1ZLDaVrhRaYgzbkXBkTPghPFsup8zDbbsYTxC5OmqrFE7skkKS/sraVsLP3TcT3Ki5CSyEFBRkLH/H/w== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.18.0" - "@typescript-eslint/types" "5.18.0" - "@typescript-eslint/typescript-estree" "5.18.0" + "@typescript-eslint/scope-manager" "5.27.1" + "@typescript-eslint/types" "5.27.1" + "@typescript-eslint/typescript-estree" "5.27.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60" - integrity sha512-Hf+t+dJsjAKpKSkg3EHvbtEpFFb/1CiOHnvI8bjHgOD4/wAw3gKrA0i94LrbekypiZVanJu3McWJg7rWDMzRTg== +"@typescript-eslint/visitor-keys@5.27.1": + version "5.27.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.1.tgz#05a62666f2a89769dac2e6baa48f74e8472983af" + integrity sha512-xYs6ffo01nhdJgPieyk7HAOpjhTsx7r/oB9LWEhwAXgwn33tkr+W8DI2ChboqhZlC4q3TC6geDYPoiX8ROqyOQ== dependencies: - "@typescript-eslint/types" "5.18.0" - eslint-visitor-keys "^3.0.0" + "@typescript-eslint/types" "5.27.1" + eslint-visitor-keys "^3.3.0" "@ungap/promise-all-settled@1.1.2": version "1.1.2" @@ -963,10 +976,10 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^7.1.1: version "7.1.1" @@ -988,11 +1001,16 @@ acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== -acorn@^8.5.0, acorn@^8.7.0: +acorn@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +acorn@^8.7.1: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -1074,7 +1092,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== @@ -1082,6 +1100,13 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1131,13 +1156,13 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" -archiver@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" - integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== +archiver@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" + integrity sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w== dependencies: archiver-utils "^2.1.0" - async "^3.2.0" + async "^3.2.3" buffer-crc32 "^0.2.1" readable-stream "^3.6.0" readdir-glob "^1.0.0" @@ -1227,27 +1252,10 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -async@0.9.x: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - -async@>=0.2.9: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== - -async@^2.6.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -async@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" - integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== +async@>=0.2.9, async@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== asynckit@^0.4.0: version "0.4.0" @@ -1266,10 +1274,10 @@ autwh@0.1.0: dependencies: oauth "0.9.15" -aws-sdk@2.1111.0: - version "2.1111.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1111.0.tgz#02b1e5c530ef8140235ee7c48c710bb2dbd7dc84" - integrity sha512-WRyNcCckzmu1djTAWfR2r+BuI/PbuLrhG3oa+oH39v4NZ4EecYWFL1CoCPlC2kRUML4maSba5T4zlxjcNl7ELQ== +aws-sdk@2.1152.0: + version "2.1152.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1152.0.tgz#73e4fb81b3a9c289234b5d6848bcdb854f169bdf" + integrity sha512-Lqwk0bDhm3vzpYb3AAM9VgGHeDpbB8+o7UJnP9R+CO23kJfi/XRpKihAcbyKDD/AUQ+O1LJaUVpvaJYLS9Am7w== dependencies: buffer "4.9.2" events "1.1.1" @@ -1278,7 +1286,7 @@ aws-sdk@2.1111.0: querystring "0.2.0" sax "1.2.1" url "0.10.3" - uuid "3.3.2" + uuid "8.0.0" xml2js "0.4.19" axios@^0.24.0: @@ -1296,9 +1304,9 @@ babel-walk@3.0.0-canary-5: "@babel/types" "^7.9.6" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base32.js@0.0.1: version "0.0.1" @@ -1327,11 +1335,6 @@ bcryptjs@2.4.3: resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= -big-integer@^1.6.16: - version "1.6.48" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" - integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== - big-integer@^1.6.17: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" @@ -1397,27 +1400,20 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1, braces@~3.0.2: +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" -broadcast-channel@4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.10.0.tgz#d19fb902df227df40b1b580351713d30c302d198" - integrity sha512-hOUh312XyHk6JTVyX9cyXaH1UYs+2gHVtnW16oQAu9FL7ALcXGXc/YoJWqlkV8vUn14URQPMmRi4A9q4UrwVEQ== - dependencies: - "@babel/runtime" "^7.16.0" - detect-node "^2.1.0" - microseconds "0.2.0" - nano-time "1.0.0" - oblivious-set "1.0.0" - p-queue "6.6.2" - rimraf "3.0.2" - unload "2.3.1" - browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -1490,10 +1486,10 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "~3.7.0" -bull@4.8.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.1.tgz#83daaefc3118876450b21d7a02bc11ea28a2440e" - integrity sha512-ojH5AfOchKQsQwwE+thViS1pMpvREGC+Ov1+3HXsQqn5Q27ZSGkgMriMqc6c9J9rvQ/+D732pZE+TN1+2LRWVg== +bull@4.8.3: + version "4.8.3" + resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.3.tgz#4ab67029fee1183dcb7185895b20dc08c02d6bf2" + integrity sha512-oOHr+KTLu3JM5V9TXsg18/1xyVQceoYCFiGrXZOpu9abZn3W3vXJtMBrwB6Yvl/RxSKVVBpoa25RF/ya3750qg== dependencies: cron-parser "^4.2.1" debuglog "^1.0.0" @@ -1586,11 +1582,6 @@ cacheable-request@^7.0.2: normalize-url "^6.0.1" responselike "^2.0.0" -cafy@15.2.1: - version "15.2.1" - resolved "https://registry.yarnpkg.com/cafy/-/cafy-15.2.1.tgz#5a55eaeb721c604c7dca652f3d555c392e5f995a" - integrity sha512-g2zOmFb63p6XcZ/zeMWKYP8YKQYNWnhJmi6K71Ql4EAFTAay31xF0PBPtdBCCfQ0fiETgWTMxKtySAVI/Od6aQ== - call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -1678,7 +1669,7 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.2: +chalk@^4.0.2, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1720,7 +1711,7 @@ cheerio@0.22.0: lodash.reject "^4.4.0" lodash.some "^4.4.0" -chokidar@3.5.3, chokidar@^3.3.1, chokidar@^3.5.2: +chokidar@3.5.3, chokidar@^3.3.1, chokidar@^3.5.3: version "3.3.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== @@ -1859,10 +1850,10 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -color@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.1.tgz#498aee5fce7fc982606c8875cab080ac0547c884" - integrity sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw== +color@^4.0.1: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== dependencies: color-convert "^2.0.1" color-string "^1.9.0" @@ -1884,10 +1875,10 @@ commander@^2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^8.2.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@^9.0.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.2.0.tgz#6e21014b2ed90d8b7c9647230d8b7a94a4a419a9" + integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w== compress-commons@^4.1.0: version "4.1.1" @@ -2079,11 +2070,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-uri-to-buffer@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" - integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== - data-uri-to-buffer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" @@ -2124,6 +2110,13 @@ debug@4.3.3: dependencies: ms "2.1.2" +debug@4.3.4, debug@^4.3.3, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2145,13 +2138,6 @@ debug@^4.3.2: dependencies: ms "2.1.2" -debug@^4.3.3: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debuglog@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -2261,26 +2247,16 @@ destroy@^1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= detect-libc@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.0.tgz#c528bc09bc6d1aa30149228240917c225448f204" integrity sha512-S55LzUl8HUav8l9E2PBTlC5PAJrHK7tkM+XXFGD+fbsbkTzhCpG6K05LxJcUOEWzMa4v6ptcMZ9s3fOdJDu0Zw== -detect-libc@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" - integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== - -detect-node@2.1.0, detect-node@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - dicer@0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" @@ -2470,12 +2446,12 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -ejs@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== +ejs@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== dependencies: - jake "^10.6.1" + jake "^10.8.5" emoji-regex@^8.0.0: version "8.0.0" @@ -2700,22 +2676,17 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint-visitor-keys@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186" - integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q== - eslint-visitor-keys@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.13.0.tgz#6fcea43b6811e655410f5626cfcf328016badcd7" - integrity sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ== +eslint@8.17.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.17.0.tgz#1cfc4b6b6912f77d24b874ca1506b0fe09328c21" + integrity sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw== dependencies: - "@eslint/eslintrc" "^1.2.1" + "@eslint/eslintrc" "^1.3.0" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -2726,14 +2697,14 @@ eslint@8.13.0: eslint-scope "^7.1.1" eslint-utils "^3.0.0" eslint-visitor-keys "^3.3.0" - espree "^9.3.1" + espree "^9.3.2" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" glob-parent "^6.0.1" - globals "^13.6.0" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" @@ -2742,7 +2713,7 @@ eslint@8.13.0: json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" regexpp "^3.2.0" @@ -2751,18 +2722,13 @@ eslint@8.13.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -esm@^3.2.22: - version "3.2.25" - resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" - integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== - -espree@^9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" - integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== +espree@^9.3.2: + version "9.3.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596" + integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA== dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" + acorn "^8.7.1" + acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" esprima@^4.0.1: @@ -2809,7 +2775,7 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter3@^4.0.4, eventemitter3@^4.0.7: +eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -2844,13 +2810,6 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - ext@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" @@ -2897,6 +2856,17 @@ fast-glob@^3.1.1: micromatch "^4.0.2" picomatch "^2.2.1" +fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -2926,11 +2896,6 @@ feed@4.2.2: dependencies: xml-js "^1.6.11" -fetch-blob@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c" - integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow== - fetch-blob@^3.1.2, fetch-blob@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.4.tgz#e8c6567f80ad7fc22fd302e7dcb72bafde9c1717" @@ -2946,21 +2911,21 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-type@17.1.1: - version "17.1.1" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-17.1.1.tgz#24c59bc663df0c0c181b31dfacde25e06431afbe" - integrity sha512-heRUMZHby2Qj6wZAA3YHeMlRmZNQTcb6VxctkGmM+mcM6ROQKvHpr7SS6EgdfEhH+s25LDshBjvPx/Ecm+bOVQ== +file-type@17.1.2: + version "17.1.2" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-17.1.2.tgz#9257437a64e0c3623f70d9f27430522d978b1384" + integrity sha512-3thBUSfa9YEUEGO/NAAiQGvjujZxZiJTF6xNwyDn6kB0NcEtwMn5ttkGG9jGwm/Nt/t8U1bpBNqyBNZCz4F4ig== dependencies: readable-web-to-node-stream "^3.0.2" strtok3 "^7.0.0-alpha.7" token-types "^5.0.0-alpha.2" filelist@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" - integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + version "1.0.3" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83" + integrity sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q== dependencies: - minimatch "^3.0.4" + minimatch "^5.0.1" fill-range@^7.0.1: version "7.0.1" @@ -2969,14 +2934,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-node-modules@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-2.1.2.tgz#57565a3455baf671b835bc6b2134a9b938b9c53c" - integrity sha512-x+3P4mbtRPlSiVE1Qco0Z4YLU8WFiFcuWTf3m75OV9Uzcfs2Bg+O9N+r/K0AnmINBW06KpfqKwYJbFlFq4qNug== - dependencies: - findup-sync "^4.0.0" - merge "^2.1.0" - find-up@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -3000,16 +2957,6 @@ find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -findup-sync@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0" - integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^4.0.2" - resolve-dir "^1.0.1" - flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3210,7 +3157,7 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -glob-parent@^5.1.0, glob-parent@~5.1.0: +glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -3248,37 +3195,10 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -globals@^13.6.0: - version "13.7.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.7.0.tgz#aed3bcefd80ad3ec0f0be2cf0c895110c0591795" - integrity sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA== - dependencies: - type-fest "^0.20.2" - -globals@^13.9.0: - version "13.9.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb" - integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA== +globals@^13.15.0: + version "13.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" + integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== dependencies: type-fest "^0.20.2" @@ -3294,6 +3214,18 @@ globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + got@11.5.1: version "11.5.1" resolved "https://registry.yarnpkg.com/got/-/got-11.5.1.tgz#bf098a270fe80b3fb88ffd5a043a59ebb0a391db" @@ -3311,10 +3243,10 @@ got@11.5.1: p-cancelable "^2.0.0" responselike "^2.0.0" -got@12.0.3: - version "12.0.3" - resolved "https://registry.yarnpkg.com/got/-/got-12.0.3.tgz#c7314daab26d42039e624adbf98f6d442e5de749" - integrity sha512-hmdcXi/S0gcAtDg4P8j/rM7+j3o1Aq6bXhjxkDhRY2ipe7PHpvx/14DgTY2czHOLaGeU8VRvRecidwfu9qdFug== +got@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/got/-/got-12.1.0.tgz#099f3815305c682be4fd6b0ee0726d8e4c6b0af4" + integrity sha512-hBv2ty9QN2RdbJJMK3hesmSkFTjVIHyIDDbssCKnSmq62edGgImJWD10Eb1k77TiV1bxloxqcFAVK8+9pkhOig== dependencies: "@sindresorhus/is" "^4.6.0" "@szmarczak/http-timer" "^5.0.1" @@ -3345,11 +3277,6 @@ graceful-fs@^4.2.6: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" @@ -3404,13 +3331,6 @@ highlight.js@^10.7.1: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360" integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg== -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - hpagent@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" @@ -3507,15 +3427,6 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" -http-signature@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" - integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== - dependencies: - assert-plus "^1.0.0" - jsprim "^2.0.2" - sshpk "^1.14.1" - http2-wrapper@^1.0.0-beta.5.0: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -3600,11 +3511,6 @@ ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== -ignore@^5.1.8: - version "5.1.9" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb" - integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ== - ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -3700,10 +3606,10 @@ ip-address@^7.1.0: jsbn "1.1.0" sprintf-js "1.1.2" -ip-cidr@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.4.tgz#a915c47e00f47ea8d5f8ed662ea6161471c44375" - integrity sha512-pKNiqmBlTvEkhaLAa3+FOmYSY0/jjADVxxjA3NbujZZTT8mjLI90Q+6mwg6kd0fNm0RuAOkWJ1u1a/ETmlrPNQ== +ip-cidr@3.0.10: + version "3.0.10" + resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.10.tgz#e1a039705196d84b43858f81a243fd70def9cefc" + integrity sha512-PXSsrRYirsuaCI1qBVyVXRLUIpNzxm76eHd3UvN5NXTMUG85GWGZpr6P+70mimc5e7Nfh/tShmjk0oSywErMWg== dependencies: ip-address "^7.1.0" jsbn "^1.1.0" @@ -3848,13 +3754,6 @@ is-negative-zero@^2.0.1: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== -is-number-like@^1.0.3: - version "1.0.8" - resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3" - integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA== - dependencies: - lodash.isfinite "^3.3.2" - is-number-object@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" @@ -3962,11 +3861,6 @@ is-whitespace@^0.3.0: resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" integrity sha1-Fjnssb4DauxppUy7QBz77XEUq38= -is-windows@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -3982,13 +3876,13 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -jake@^10.6.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" - integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== dependencies: - async "0.9.x" - chalk "^2.4.2" + async "^3.2.3" + chalk "^4.0.2" filelist "^1.0.1" minimatch "^3.0.4" @@ -4117,7 +4011,7 @@ json5-loader@4.0.1: loader-utils "^2.0.0" schema-utils "^3.0.0" -json5@2.2.1: +json5@2.2.1, json5@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== @@ -4152,30 +4046,30 @@ jsonfile@^5.0.0: optionalDependencies: graceful-fs "^4.1.6" -jsonld@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-5.2.0.tgz#d1e8af38a334cb95edf0f2ae4e2b58baf8d2b5a9" - integrity sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw== +jsonld@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-6.0.0.tgz#560a8a871dce72aba5d4c6b08356438d863d62fb" + integrity sha512-1SkN2RXhMCTCSkX+bzHvr9ycM2HTmjWyV41hn2xG7k6BqlCgRjw0zHmuqfphjBRPqi1gKMIqgBCe/0RZMcWrAA== dependencies: - "@digitalbazaar/http-client" "^1.1.0" + "@digitalbazaar/http-client" "^3.2.0" canonicalize "^1.0.1" lru-cache "^6.0.0" rdf-canonize "^3.0.0" -jsprim@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" - integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== dependencies: assert-plus "1.0.0" extsprintf "1.3.0" json-schema "0.4.0" verror "1.10.0" -jsrsasign@8.0.20: - version "8.0.20" - resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.20.tgz#37d8029c9d8f794d8ac8d8998bce319921491f11" - integrity sha512-JTXt9+nqdynIB8wFsS6e8ffHhIjilhywXwdaEVHSj9OVmwldG2H0EoCqkQ+KXkm2tVqREfH/HEmklY4k1/6Rcg== +jsrsasign@10.5.24: + version "10.5.24" + resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.24.tgz#2d159e1756b2268682c6eb5e147184e33e946b1c" + integrity sha512-0i/UHRgJZifp/YmoXHyNQXUY4eKWiSd7YxuD7oKEw9mlqgr51hg9lZQw2nlEDvwHDh7pyj6ZjYlxldlW27xb/Q== jstransformer@1.0.0: version "1.0.0" @@ -4368,18 +4262,18 @@ koa@2.13.4, koa@^2.13.1: type-is "^1.6.16" vary "^1.1.2" -ky-universal@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.8.2.tgz#edc398d54cf495d7d6830aa1ab69559a3cc7f824" - integrity sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ== +ky-universal@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.10.1.tgz#778881e098f6e3c52a87b382d9acca54d22bb0d3" + integrity sha512-r8909k+ELKZAxhVA5c440x22hqw5XcMRwLRbgpPQk4JHy3/ddJnvzcnSo5Ww3HdKdNeS3Y8dBgcIYyVahMa46g== dependencies: abort-controller "^3.0.0" - node-fetch "3.0.0-beta.9" + node-fetch "^3.2.2" -ky@^0.25.1: - version "0.25.1" - resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" - integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== +ky@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.30.0.tgz#a3d293e4f6c4604a9a4694eceb6ce30e73d27d64" + integrity sha512-X/u76z4JtDVq10u1JA5UQfatPxgPaVDMYTrgHyiTpGN2z4TMEJkIHsoSBBSg9SWZEIXTKsi9kHgiQ9o3Y/4yog== lazystream@^1.0.0: version "1.0.1" @@ -4485,11 +4379,6 @@ lodash.isequal@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= -lodash.isfinite@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3" - integrity sha1-+4m2WpqAKBgz8LdHizpRBPiY67M= - lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -4535,7 +4424,7 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21: +lodash@^4.17.11, lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4573,11 +4462,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.4.0: - version "7.8.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.8.1.tgz#68ee3f4807a57d2ba185b7fd90827d5c21ce82bb" - integrity sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg== - luxon@^1.28.0: version "1.28.0" resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" @@ -4625,27 +4509,22 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -merge@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98" - integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== - methods@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= -mfm-js@0.21.0: - version "0.21.0" - resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.21.0.tgz#954cc6e7071700b0b1872c78a90bada10be7f772" - integrity sha512-nyQXaipa7rmAw9ER9uYigMvGcdCwhSv93abZBwccnSnPOc1W3S/WW0+sN28g3YSmlHDCA0i2q9aAFc9EgOi5KA== +mfm-js@0.22.1: + version "0.22.1" + resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.22.1.tgz#ad5f0b95cc903ca5a5e414e2edf64ac4648dc8c2" + integrity sha512-UV5zvDKlWPpBFeABhyCzuOTJ3RwrNrmVpJ+zz/dFX6D/ntEywljgxkfsLamcy0ZSwUAr0O+WQxGHvAwyxUgsAQ== dependencies: - twemoji-parser "13.1.x" + twemoji-parser "14.0.x" micromatch@^4.0.0, micromatch@^4.0.2: version "4.0.2" @@ -4655,10 +4534,13 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" -microseconds@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" - integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" mime-db@1.44.0: version "1.44.0" @@ -4704,21 +4586,14 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" - integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== +minimatch@5.0.1, minimatch@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -4815,40 +4690,38 @@ mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mocha@9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" - integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== +mocha@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" + integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== dependencies: "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" chokidar "3.5.3" - debug "4.3.3" + debug "4.3.4" diff "5.0.0" escape-string-regexp "4.0.0" find-up "5.0.0" glob "7.2.0" - growl "1.10.5" he "1.2.0" js-yaml "4.1.0" log-symbols "4.1.0" - minimatch "4.2.1" + minimatch "5.0.1" ms "2.1.3" - nanoid "3.3.1" + nanoid "3.3.3" serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" - which "2.0.2" - workerpool "6.2.0" + workerpool "6.2.1" yargs "16.2.0" yargs-parser "20.2.4" yargs-unparser "2.0.0" moment@^2.22.2: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + version "2.29.3" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" + integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== ms@2.0.0: version "2.0.0" @@ -4899,10 +4772,10 @@ multer@1.4.4: type-is "^1.6.4" xtend "^4.0.0" -mylas@^2.1.4: - version "2.1.5" - resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.5.tgz#7ccf41ec5a93ab2d63fc3678abf1942c0e7bdeb1" - integrity sha512-7ZyrJux1lipSR45IxDvWz7zJOXWTazTFCqD4/p8XBF4O+mtJwf7QpMWTH+jE4lV9O2I38xcpS0KTIp7GwhUTmA== +mylas@^2.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.9.tgz#8329626f95c0ce522ca7d3c192eca6221d172cdc" + integrity sha512-pa+cQvmhoM8zzgitPYZErmDt9EdTNVnXsH1XFjMeM4TyG4FFcgxrvK1+jwabVFwUOEDaSWuXBMjg43kqt/Ydlg== mz@^2.4.0, mz@^2.7.0: version "2.7.0" @@ -4918,14 +4791,12 @@ nan@^2.14.2, nan@^2.15.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nano-time@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" - integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= - dependencies: - big-integer "^1.6.16" +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -nanoid@3.3.1, nanoid@^3.1.30: +nanoid@^3.1.30: version "3.3.1" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== @@ -4976,7 +4847,7 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" -node-addon-api@^4.3.0: +node-addon-api@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== @@ -4995,18 +4866,10 @@ node-fetch@*: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" -node-fetch@3.0.0-beta.9: - version "3.0.0-beta.9" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0-beta.9.tgz#0a7554cfb824380dd6812864389923c783c80d9b" - integrity sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg== - dependencies: - data-uri-to-buffer "^3.0.1" - fetch-blob "^2.1.1" - -node-fetch@3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.3.tgz#a03c9cc2044d21d1a021566bd52f080f333719a6" - integrity sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA== +node-fetch@3.2.6, node-fetch@^3.2.2: + version "3.2.6" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.6.tgz#6d4627181697a9d9674aae0d61548e0d629b31b9" + integrity sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw== dependencies: data-uri-to-buffer "^4.0.0" fetch-blob "^3.1.4" @@ -5045,10 +4908,10 @@ node-gyp@^8.4.1: tar "^6.1.2" which "^2.0.2" -nodemailer@6.7.3: - version "6.7.3" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.3.tgz#b73f9a81b9c8fa8acb4ea14b608f5e725ea8e018" - integrity sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g== +nodemailer@6.7.5: + version "6.7.5" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.5.tgz#b30b1566f5fa2249f7bd49ced4c58bec6b25915e" + integrity sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg== nofilter@^2.0.3: version "2.0.3" @@ -5175,11 +5038,6 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -oblivious-set@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" - integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== - on-finished@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -5327,14 +5185,6 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-queue@6.6.2: - version "6.6.2" - resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" - integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== - dependencies: - eventemitter3 "^4.0.4" - p-timeout "^3.2.0" - p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -5364,11 +5214,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - parse-srcset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" @@ -5507,11 +5352,23 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +plimit-lit@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/plimit-lit/-/plimit-lit-1.2.6.tgz#8c1336f26a042b6e9f1acc665be5eee4c2a55fb3" + integrity sha512-EuVnKyDeFgr58aidKf2G7DI41r23bxphlvBKAZ8e8dT9of0Ez2g9w6JbJGUP1YBNC2yG9+ZCCbjLj4yS1P5Gzw== + dependencies: + queue-lit "^1.2.7" + pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" @@ -5527,14 +5384,6 @@ pngjs@^5.0.0: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== -portscanner@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1" - integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw== - dependencies: - async "^2.6.0" - is-number-like "^1.0.3" - postcss@^8.3.11: version "8.3.11" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858" @@ -5566,10 +5415,10 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" -prebuild-install@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870" - integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg== +prebuild-install@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370" + integrity sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA== dependencies: detect-libc "^2.0.0" expand-template "^2.0.3" @@ -5828,6 +5677,11 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= +queue-lit@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/queue-lit/-/queue-lit-1.2.7.tgz#69081656c9e7b81f09770bb2de6aa007f1a90763" + integrity sha512-K/rTdggORRcmf3+c89ijPlgJ/ldGP4oBj6Sm7VcTup4B2clf03Jo8QaXTnMst4EEQwkUbOZFN4frKocq2I85gw== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" @@ -6006,11 +5860,6 @@ reflect-metadata@0.1.13, reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== -regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -6053,14 +5902,6 @@ resolve-alpn@^1.2.0: resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -6115,7 +5956,7 @@ rimraf@2: dependencies: glob "^7.1.3" -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -6213,12 +6054,12 @@ seedrandom@3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -semver@7.3.6: - version "7.3.6" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.6.tgz#5d73886fb9c0c6602e79440b97165c29581cbb2b" - integrity sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w== +semver@7.3.7, semver@^7.3.7: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: - lru-cache "^7.4.0" + lru-cache "^6.0.0" semver@^5.6.0: version "5.7.1" @@ -6274,17 +6115,17 @@ sha.js@^2.4.11: inherits "^2.0.1" safe-buffer "^5.0.1" -sharp@0.30.3: - version "0.30.3" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.3.tgz#315a1817423a4d1cde5119a21c99c234a7a6fb37" - integrity sha512-rjpfJFK58ZOFSG8sxYSo3/JQb4ej095HjXp9X7gVu7gEn1aqSG8TCW29h/Rr31+PXrFADo1H/vKfw0uhMQWFtg== +sharp@0.29.3: + version "0.29.3" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.3.tgz#0da183d626094c974516a48fab9b3e4ba92eb5c2" + integrity sha512-fKWUuOw77E4nhpyzCCJR1ayrttHoFHBT2U/kR/qEMRhvPEcluG4BKj324+SCO1e84+knXHwhJ1HHJGnUt4ElGA== dependencies: - color "^4.2.1" - detect-libc "^2.0.1" - node-addon-api "^4.3.0" - prebuild-install "^7.0.1" + color "^4.0.1" + detect-libc "^1.0.3" + node-addon-api "^4.2.0" + prebuild-install "^7.0.0" semver "^7.3.5" - simple-get "^4.0.1" + simple-get "^4.0.0" tar-fs "^2.1.1" tunnel-agent "^0.6.0" @@ -6329,7 +6170,7 @@ simple-concat@^1.0.0: resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== -simple-get@^4.0.0, simple-get@^4.0.1: +simple-get@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== @@ -6588,16 +6429,17 @@ style-loader@3.3.1: resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== -summaly@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.0.tgz#ec5af6e84857efcb6c844d896e83569e64a923ea" - integrity sha512-IzvO2s7yj/PUyH42qWjVjSPpIiPlgTRWGh33t4cIZKOqPQJ2INo7e83hXhHFr4hXTb3JRcIdCuM1ELjlrujiUQ== +summaly@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.1.tgz#742fe6631987f84ad2e95d2b0f7902ec57e0f6b3" + integrity sha512-WWvl7rLs3wm61Xc2JqgTbSuqtIOmGqKte+rkbnxe6ISy4089lQ+7F2ajooQNee6PWHl9kZ27SDd1ZMoL3/6R4A== dependencies: cheerio "0.22.0" debug "4.3.3" escape-regexp "0.0.1" got "11.5.1" html-entities "2.3.2" + iconv-lite "0.6.3" jschardet "3.0.0" koa "2.13.4" private-ip "2.3.3" @@ -6642,10 +6484,10 @@ syslog-pro@1.0.0: dependencies: moment "^2.22.2" -systeminformation@5.11.9: - version "5.11.9" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.9.tgz#95f2334e739dd224178948a2afaced7d9abfdf9d" - integrity sha512-eeMtL9UJFR/LYG+2rpeAgZ0Va4ojlNQTkYiQH/xbbPwDjDMsaetj3Pkc+C1aH5G8mav6HvDY8kI4Vl4noksSkA== +systeminformation@5.11.16: + version "5.11.16" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.16.tgz#5f6fda2447fafe204bd2ab543475f1ffa8c14a85" + integrity sha512-/a1VfP9WELKLT330yhAHJ4lWCXRYynel1kMMHKc/qdzCgDt3BIcMlo+3tKcTiRHFefjV3fz4AvqMx7dGO/72zw== tapable@^2.2.0: version "2.2.0" @@ -6803,22 +6645,22 @@ trace-redirect@1.0.6: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= -ts-loader@9.2.8: - version "9.2.8" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.8.tgz#e89aa32fa829c5cad0a1d023d6b3adecd51d5a48" - integrity sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw== +ts-loader@9.3.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.0.tgz#980f4dbfb60e517179e15e10ed98e454b132159f" + integrity sha512-2kLLAdAD+FCKijvGKi9sS0OzoqxLCF3CxHpok7rVgCZ5UldRzH0TkbwG9XECKjBzHsAewntC5oDaI/FwKzEUog== dependencies: chalk "^4.1.0" enhanced-resolve "^5.0.0" micromatch "^4.0.0" semver "^7.3.4" -ts-node@10.7.0: - version "10.7.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" - integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== +ts-node@10.8.1: + version "10.8.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.1.tgz#ea2bd3459011b52699d7e88daa55a45a1af4f066" + integrity sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g== dependencies: - "@cspotcode/source-map-support" "0.7.0" + "@cspotcode/source-map-support" "^0.8.0" "@tsconfig/node10" "^1.0.7" "@tsconfig/node12" "^1.0.7" "@tsconfig/node14" "^1.0.0" @@ -6829,22 +6671,31 @@ ts-node@10.7.0: create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" - v8-compile-cache-lib "^3.0.0" + v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tsc-alias@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.4.1.tgz#6a6075dd94267d9befdad1431f410bd0b8819805" - integrity sha512-nHTR8qvM/LiYI8Fx6UrzAQXRngAuE2PEK+n9uXmQY6fN+oLZhweNFkCLbyxKDmlLfYnclSuaR+dSuvRd7FUu8Q== +tsc-alias@1.6.9: + version "1.6.9" + resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.6.9.tgz#d04d95124b95ad8eea55e52d45cf65a744c26baa" + integrity sha512-5lv5uAHn0cgxY1XfpXIdquUSz2xXq3ryQyNtxC6DYH7YT5rt/W+9Gsft2uyLFTh+ozk4qU8iCSP3VemjT69xlQ== dependencies: - chokidar "^3.5.2" - commander "^8.2.0" - find-node-modules "^2.1.2" + chokidar "^3.5.3" + commander "^9.0.0" globby "^11.0.4" - mylas "^2.1.4" + mylas "^2.1.9" normalize-path "^3.0.0" + plimit-lit "^1.2.6" -tsconfig-paths@3.14.1, tsconfig-paths@^3.14.1: +tsconfig-paths@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.0.0.tgz#1082f5d99fd127b72397eef4809e4dd06d229b64" + integrity sha512-SLBg2GBKlR6bVtMgJJlud/o3waplKtL7skmLkExomIiaAtLGtVsoXIqP3SYdjbcH9lq/KVv7pMZeCBpLYOit6Q== + dependencies: + json5 "^2.2.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== @@ -6888,12 +6739,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -twemoji-parser@13.1.0, twemoji-parser@13.1.x: - version "13.1.0" - resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4" - integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg== - -twemoji-parser@14.0.0: +twemoji-parser@14.0.0, twemoji-parser@14.0.x: version "14.0.0" resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-14.0.0.tgz#13dabcb6d3a261d9efbf58a1666b182033bf2b62" integrity sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA== @@ -6952,10 +6798,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typeorm@0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.5.tgz#8fe50d517de5ec6f4b38856ea0f180e4a60cf7e4" - integrity sha512-KL4c8nQqouHaXs4m1J3xh7oXWqX4+A9poExbceLxBRtlavpJQYqiSnqt3JYGpy7Tl9vD5DG5DrmZrSslTkkW5Q== +typeorm@0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.6.tgz#65203443a1b684bb746785913fe2b0877aa991c0" + integrity sha512-DRqgfqcelMiGgWSMbBmVoJNFN2nPNA3EeY2gC324ndr2DZoGRTb9ILtp2oGVGnlA+cu5zgQ6it5oqKFNkte7Aw== dependencies: "@sqltools/formatter" "^1.2.2" app-root-path "^3.0.0" @@ -6975,10 +6821,10 @@ typeorm@0.3.5: xml2js "^0.4.23" yargs "^17.3.1" -typescript@4.6.3: - version "4.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" - integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== +typescript@4.7.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" + integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== ulid@2.3.0: version "2.3.0" @@ -6995,6 +6841,11 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +undici@^5.2.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.4.0.tgz#c474fae02743d4788b96118d46008a24195024d2" + integrity sha512-A1SRXysDg7J+mVP46jF+9cKANw0kptqSFZ8tGyL+HBiv0K1spjxPX8Z4EGu+Eu6pjClJUBdnUPlxrOafR668/g== + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -7014,14 +6865,6 @@ universalify@^0.1.0, universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -unload@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/unload/-/unload-2.3.1.tgz#9d16862d372a5ce5cb630ad1309c2fd6e35dacfe" - integrity sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA== - dependencies: - "@babel/runtime" "^7.6.2" - detect-node "2.1.0" - unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -7075,25 +6918,25 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - uuid@7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== +uuid@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -v8-compile-cache-lib@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" - integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== v8-compile-cache@^2.0.3: version "2.2.0" @@ -7133,10 +6976,10 @@ w3c-xmlserializer@^3.0.0: dependencies: xml-name-validator "^4.0.0" -web-push@3.4.5: - version "3.4.5" - resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.5.tgz#f94074ff150538872c7183e4d8881c8305920cf1" - integrity sha512-2njbTqZ6Q7ZqqK14YpK1GGmaZs3NmuGYF5b7abCXulUIWFSlSYcZ3NBJQRFcMiQDceD7vQknb8FUuvI1F7Qe/g== +web-push@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.5.0.tgz#4576533746052eda3bd50414b54a1b0a21eeaeae" + integrity sha512-JC0V9hzKTqlDYJ+LTZUXtW7B175qwwaqzbbMSWDxHWxZvd3xY0C2rcotMGDavub2nAAFw+sXTsqR65/KY2A5AQ== dependencies: asn1.js "^5.3.0" http_ece "1.1.0" @@ -7216,20 +7059,20 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@2.0.2, which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -which@^1.1.1, which@^1.2.14: +which@^1.1.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -7259,10 +7102,10 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -workerpool@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" - integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== wrap-ansi@^6.2.0: version "6.2.0" @@ -7287,20 +7130,20 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" - integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== +ws@8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769" + integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ== ws@^8.2.3: version "8.4.2" resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b" integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA== -xev@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/xev/-/xev-2.0.1.tgz#24484173a22115bc8a990ef5d4d5129695b827a7" - integrity sha512-icDf9M67bDge0F2qf02WKZq+s7mMO/SbPv67ZQPym6JThLEOdlWWLdB7VTVgRJp3ekgaiVItCAyH6aoKCPvfIA== +xev@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/xev/-/xev-3.0.2.tgz#3f4080bd8bed0d3479c674050e3696da98d22a4d" + integrity sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw== xml-js@^1.6.11: version "1.6.11" diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js index a6e23e5171..10f0e5a9cb 100644 --- a/packages/client/.eslintrc.js +++ b/packages/client/.eslintrc.js @@ -1,68 +1,85 @@ module.exports = { root: true, env: { - "node": false + 'node': false, }, - parser: "vue-eslint-parser", + parser: 'vue-eslint-parser', parserOptions: { - "parser": "@typescript-eslint/parser", + 'parser': '@typescript-eslint/parser', tsconfigRootDir: __dirname, - //project: ['./tsconfig.json'], + project: ['./tsconfig.json'], + extraFileExtensions: ['.vue'], }, extends: [ - //"../shared/.eslintrc.js", - "plugin:vue/vue3-recommended" + '../shared/.eslintrc.js', + 'plugin:vue/vue3-recommended', ], rules: { + '@typescript-eslint/no-empty-interface': [ + 'error', + { + 'allowSingleExtends': true, + }, + ], // window ใฎ็ฆๆญข็†็”ฑ: ใ‚ฐใƒญใƒผใƒใƒซใ‚นใ‚ณใƒผใƒ—ใจ่ก็ชใ—ใ€ไบˆๆœŸใ›ใฌ็ตๆžœใ‚’ๆ‹›ใใŸใ‚ // data ใฎ็ฆๆญข็†็”ฑ: ๆŠฝ่ฑก็š„ใ™ใŽใ‚‹ใŸใ‚ // e ใฎ็ฆๆญข็†็”ฑ: error ใ‚„ event ใชใฉใ€่ค‡ๆ•ฐใฎใ‚ญใƒผใƒฏใƒผใƒ‰ใฎ้ ญๆ–‡ๅญ—ใงใ‚ใ‚Šๅˆ†ใ‹ใ‚Šใซใใ„ใŸใ‚ - "id-denylist": ["error", "window", "data", "e"], + 'id-denylist': ['error', 'window', 'data', 'e'], 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], - "no-shadow": ["warn"], - "vue/attributes-order": ["error", { - "alphabetical": false + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + 'alphabetical': false, }], - "vue/no-use-v-if-with-v-for": ["error", { - "allowUsingIterationVar": false + 'vue/no-use-v-if-with-v-for': ['error', { + 'allowUsingIterationVar': false, }], - "vue/no-ref-as-operand": "error", - "vue/no-multi-spaces": ["error", { - "ignoreProperties": false + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + 'ignoreProperties': false, }], - "vue/no-v-html": "error", - "vue/order-in-components": "error", - "vue/html-indent": ["warn", "tab", { - "attribute": 1, - "baseIndent": 0, - "closeBracket": 0, - "alignAttributesVertically": true, - "ignores": [] + 'vue/no-v-html': 'error', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + 'attribute': 1, + 'baseIndent': 0, + 'closeBracket': 0, + 'alignAttributesVertically': true, + 'ignores': [], }], - "vue/html-closing-bracket-spacing": ["warn", { - "startTag": "never", - "endTag": "never", - "selfClosingTag": "never" + 'vue/html-closing-bracket-spacing': ['warn', { + 'startTag': 'never', + 'endTag': 'never', + 'selfClosingTag': 'never', }], - "vue/multi-word-component-names": "warn", - "vue/require-v-for-key": "warn", - "vue/no-unused-components": "warn", - "vue/valid-v-for": "warn", - "vue/return-in-computed-property": "warn", - "vue/no-setup-props-destructure": "warn", - "vue/max-attributes-per-line": "off", - "vue/html-self-closing": "off", - "vue/singleline-html-element-content-newline": "off", + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-destructure': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', }, globals: { - "require": false, - "_DEV_": false, - "_LANGS_": false, - "_VERSION_": false, - "_ENV_": false, - "_PERF_PREFIX_": false, - "_DATA_TRANSFER_DRIVE_FILE_": false, - "_DATA_TRANSFER_DRIVE_FOLDER_": false, - "_DATA_TRANSFER_DECK_COLUMN_": false - } -} + // Node.js + 'module': false, + 'require': false, + '__dirname': false, + + // Vue + '$$': false, + '$ref': false, + '$computed': false, + + // Misskey + '_DEV_': false, + '_LANGS_': false, + '_VERSION_': false, + '_ENV_': false, + '_PERF_PREFIX_': false, + '_DATA_TRANSFER_DRIVE_FILE_': false, + '_DATA_TRANSFER_DRIVE_FOLDER_': false, + '_DATA_TRANSFER_DECK_COLUMN_': false, + }, +}; diff --git a/packages/client/@types/theme.d.ts b/packages/client/@types/theme.d.ts new file mode 100644 index 0000000000..67f724a9aa --- /dev/null +++ b/packages/client/@types/theme.d.ts @@ -0,0 +1,7 @@ +declare module '@/themes/*.json5' { + import { Theme } from "@/scripts/theme"; + + const theme: Theme; + + export default theme; +} diff --git a/packages/client/package.json b/packages/client/package.json index 9de500f3ab..83c8086e23 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,80 +1,52 @@ { "private": true, "scripts": { - "watch": "webpack --watch", - "build": "webpack", - "lint": "eslint --quiet 'src/**/*.{ts,vue}'" + "watch": "vite build --watch --mode development", + "build": "vite build", + "lint": "eslint --quiet \"src/**/*.{ts,vue}\"" }, "resolutions": { "chokidar": "^3.3.1", "lodash": "^4.17.21" }, "dependencies": { - "@discordapp/twemoji": "13.1.1", + "@discordapp/twemoji": "14.0.2", "@fortawesome/fontawesome-free": "6.1.1", + "@rollup/plugin-alias": "3.1.9", + "@rollup/plugin-json": "4.1.0", "@syuilo/aiscript": "0.11.1", - "@types/escape-regexp": "0.0.1", - "@types/glob": "7.2.0", - "@types/gulp": "4.0.9", - "@types/gulp-rename": "2.0.1", - "@types/is-url": "1.2.30", - "@types/katex": "0.14.0", - "@types/matter-js": "0.17.7", - "@types/mocha": "9.1.0", - "@types/oauth": "0.9.1", - "@types/parse5": "6.0.3", - "@types/punycode": "2.1.0", - "@types/qrcode": "1.4.2", - "@types/random-seed": "0.3.3", - "@types/seedrandom": "3.0.2", - "@types/throttle-debounce": "2.1.0", - "@types/tinycolor2": "1.4.3", - "@types/uuid": "8.3.4", - "@types/webpack": "5.28.0", - "@types/webpack-stream": "3.2.12", - "@types/websocket": "1.0.5", - "@types/ws": "8.5.3", - "@typescript-eslint/parser": "5.18.0", - "@vue/compiler-sfc": "3.2.31", + "@vitejs/plugin-vue": "2.3.3", + "@vue/compiler-sfc": "3.2.37", "abort-controller": "3.0.0", "autobind-decorator": "2.4.0", "autosize": "5.0.1", "autwh": "0.1.0", "blurhash": "1.1.5", - "broadcast-channel": "4.10.0", - "chart.js": "3.7.1", + "broadcast-channel": "4.13.0", + "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2", + "chart.js": "3.8.0", "chartjs-adapter-date-fns": "2.0.0", - "chartjs-plugin-gradient": "0.2.2", + "chartjs-plugin-gradient": "0.5.0", "chartjs-plugin-zoom": "1.2.1", "compare-versions": "4.1.3", "content-disposition": "0.5.4", - "css-loader": "6.7.1", - "cssnano": "5.1.7", + "cropperjs": "2.0.0-beta", "date-fns": "2.28.0", "escape-regexp": "0.0.1", - "eslint": "8.13.0", - "eslint-plugin-vue": "8.6.0", "eventemitter3": "4.0.7", "feed": "4.2.2", - "glob": "7.2.0", "idb-keyval": "6.1.0", "insert-text-at-cursor": "0.3.0", - "ip-cidr": "3.0.4", "json5": "2.2.1", - "json5-loader": "4.0.1", - "katex": "0.15.3", + "katex": "0.15.6", "matter-js": "0.18.0", - "mfm-js": "0.21.0", + "mfm-js": "0.22.1", "misskey-js": "0.0.14", - "mocha": "9.2.2", + "mocha": "10.0.0", "ms": "2.1.3", "nested-property": "4.0.0", - "parse5": "6.0.1", - "photoswipe": "5.2.4", - "portscanner": "2.2.0", - "postcss": "8.4.12", - "postcss-loader": "6.2.1", - "prismjs": "1.27.0", + "photoswipe": "5.2.7", + "prismjs": "1.28.0", "private-ip": "2.3.3", "promise-limit": "2.7.0", "pug": "3.0.2", @@ -84,43 +56,58 @@ "random-seed": "0.3.0", "reflect-metadata": "0.1.13", "rndstr": "1.0.0", + "rollup": "2.75.6", "s-age": "1.1.2", - "sass": "1.50.0", - "sass-loader": "12.6.0", + "sass": "1.52.3", "seedrandom": "3.0.5", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "style-loader": "3.3.1", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "three": "0.139.2", - "throttle-debounce": "4.0.0", + "three": "0.141.0", + "throttle-debounce": "5.0.0", "tinycolor2": "1.4.2", - "ts-loader": "9.2.8", - "tsc-alias": "1.5.0", - "tsconfig-paths": "3.14.1", + "tsc-alias": "1.6.9", + "tsconfig-paths": "4.0.0", "twemoji-parser": "14.0.0", - "typescript": "4.6.3", + "typescript": "4.7.3", "uuid": "8.3.2", "v-debounce": "0.1.2", "vanilla-tilt": "1.7.2", - "vue": "3.2.31", - "vue-loader": "17.0.0", + "vite": "2.9.10", + "vue": "3.2.37", "vue-prism-editor": "2.0.0-alpha.2", - "vue-router": "4.0.14", - "vue-style-loader": "4.1.3", - "vue-svg-loader": "0.17.0-beta.2", + "vue-router": "4.0.16", "vuedraggable": "4.0.1", - "webpack": "5.72.0", - "webpack-cli": "4.9.2", "websocket": "1.0.34", - "ws": "8.5.0" + "ws": "8.8.0" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "5.18.0", + "@types/escape-regexp": "0.0.1", + "@types/glob": "7.2.0", + "@types/gulp": "4.0.9", + "@types/gulp-rename": "2.0.1", + "@types/is-url": "1.2.30", + "@types/katex": "0.14.0", + "@types/matter-js": "0.17.7", + "@types/mocha": "9.1.1", + "@types/oauth": "0.9.1", + "@types/punycode": "2.1.0", + "@types/qrcode": "1.4.2", + "@types/random-seed": "0.3.3", + "@types/seedrandom": "3.0.2", + "@types/throttle-debounce": "5.0.0", + "@types/tinycolor2": "1.4.3", + "@types/uuid": "8.3.4", + "@types/websocket": "1.0.5", + "@types/ws": "8.5.3", + "@typescript-eslint/eslint-plugin": "5.27.1", + "@typescript-eslint/parser": "5.27.1", "cross-env": "7.0.3", - "cypress": "9.5.3", + "cypress": "10.0.3", + "eslint": "8.17.0", "eslint-plugin-import": "2.26.0", + "eslint-plugin-vue": "9.1.0", "start-server-and-test": "1.14.0" } } diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index f4dcab319c..ce4af61f18 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -1,5 +1,5 @@ import { del, get, set } from '@/scripts/idb-proxy'; -import { reactive } from 'vue'; +import { defineAsyncComponent, reactive } from 'vue'; import * as misskey from 'misskey-js'; import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; @@ -11,10 +11,10 @@ import { i18n } from './i18n'; type Account = misskey.entities.MeDetailed; -const data = localStorage.getItem('account'); +const accountData = localStorage.getItem('account'); // TODO: ๅค–้ƒจใ‹ใ‚‰ใฏreadonlyใซ -export const $i = data ? reactive(JSON.parse(data) as Account) : null; +export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); @@ -52,7 +52,7 @@ export async function signout() { return Promise.all(registrations.map(registration => registration.unregister())); }); } - } catch (e) {} + } catch (err) {} //#endregion document.cookie = `igi=; path=/`; @@ -104,8 +104,8 @@ function fetchAccount(token: string): Promise { }); } -export function updateAccount(data) { - for (const [key, value] of Object.entries(data)) { +export function updateAccount(accountData) { + for (const [key, value] of Object.entries(accountData)) { $i[key] = value; } localStorage.setItem('account', JSON.stringify($i)); @@ -141,7 +141,7 @@ export async function openAccountMenu(opts: { onChoose?: (account: misskey.entities.UserDetailed) => void; }, ev: MouseEvent) { function showSigninDialog() { - popup(import('@/components/signin-dialog.vue'), {}, { + popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { done: res => { addAccount(res.id, res.i); success(); @@ -150,7 +150,7 @@ export async function openAccountMenu(opts: { } function createAccount() { - popup(import('@/components/signup-dialog.vue'), {}, { + popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, { done: res => { addAccount(res.id, res.i); switchAccountWithToken(res.i); diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue index f2cb369802..5114349620 100644 --- a/packages/client/src/components/abuse-report-window.vue +++ b/packages/client/src/components/abuse-report-window.vue @@ -37,7 +37,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); const window = ref>(); diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue index b67cef209b..a947406f88 100644 --- a/packages/client/src/components/abuse-report.vue +++ b/packages/client/src/components/abuse-report.vue @@ -2,7 +2,7 @@

- + @@ -43,20 +43,20 @@ export default defineComponent({ MkSwitch, }, - emits: ['resolved'], - props: { report: { type: Object, required: true, } - } + }, + + emits: ['resolved'], data() { return { forward: this.report.forwarded, }; - } + }, methods: { acct, diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue index 59b8e97304..18dd1e3f41 100644 --- a/packages/client/src/components/analog-clock.vue +++ b/packages/client/src/components/analog-clock.vue @@ -42,7 +42,7 @@ diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue index e7c9aabe4e..dd906f9bf3 100644 --- a/packages/client/src/components/cw-button.vue +++ b/packages/client/src/components/cw-button.vue @@ -18,7 +18,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'update:modelValue', v: boolean): void; + (ev: 'update:modelValue', v: boolean): void; }>(); const label = computed(() => { diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue index 3e106a4f0c..b090f3cb4e 100644 --- a/packages/client/src/components/dialog.vue +++ b/packages/client/src/components/dialog.vue @@ -90,8 +90,8 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'done', v: { canceled: boolean; result: any }): void; - (e: 'closed'): void; + (ev: 'done', v: { canceled: boolean; result: any }): void; + (ev: 'closed'): void; }>(); const modal = ref>(); @@ -122,14 +122,14 @@ function onBgClick() { if (props.cancelableByBgClick) cancel(); } */ -function onKeydown(e: KeyboardEvent) { - if (e.key === 'Escape') cancel(); +function onKeydown(evt: KeyboardEvent) { + if (evt.key === 'Escape') cancel(); } -function onInputKeydown(e: KeyboardEvent) { - if (e.key === 'Enter') { - e.preventDefault(); - e.stopPropagation(); +function onInputKeydown(evt: KeyboardEvent) { + if (evt.key === 'Enter') { + evt.preventDefault(); + evt.stopPropagation(); ok(); } } diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue index 81b80e7e8e..dd24440e82 100644 --- a/packages/client/src/components/drive-file-thumbnail.vue +++ b/packages/client/src/components/drive-file-thumbnail.vue @@ -42,7 +42,7 @@ const is = computed(() => { "application/x-tar", "application/gzip", "application/x-7z-compressed" - ].some(e => e === props.file.type)) return 'archive'; + ].some(archiveType => archiveType === props.file.type)) return 'archive'; return 'unknown'; }); diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue index f6c59457d1..03974559d2 100644 --- a/packages/client/src/components/drive-select-dialog.vue +++ b/packages/client/src/components/drive-select-dialog.vue @@ -33,8 +33,8 @@ withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'done', r?: Misskey.entities.DriveFile[]): void; - (e: 'closed'): void; + (ev: 'done', r?: Misskey.entities.DriveFile[]): void; + (ev: 'closed'): void; }>(); const dialog = ref>(); diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue index d08c5fb674..5bbfca83c9 100644 --- a/packages/client/src/components/drive-window.vue +++ b/packages/client/src/components/drive-window.vue @@ -24,6 +24,6 @@ defineProps<{ }>(); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue index 262eae0de1..aaf7ca3ca3 100644 --- a/packages/client/src/components/drive.file.vue +++ b/packages/client/src/components/drive.file.vue @@ -31,7 +31,7 @@ diff --git a/packages/client/src/components/global/avatar.vue b/packages/client/src/components/global/avatar.vue index 27cfb6e4d4..4868896c99 100644 --- a/packages/client/src/components/global/avatar.vue +++ b/packages/client/src/components/global/avatar.vue @@ -32,7 +32,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'click', ev: MouseEvent): void; + (ev: 'click', v: MouseEvent): void; }>(); const url = $computed(() => defaultStore.state.disableShowingAnimatedImages diff --git a/packages/client/src/components/global/emoji.vue b/packages/client/src/components/global/emoji.vue index 92edb1caf9..0075e0867d 100644 --- a/packages/client/src/components/global/emoji.vue +++ b/packages/client/src/components/global/emoji.vue @@ -46,7 +46,7 @@ export default defineComponent({ const url = computed(() => { if (char.value) { let codes = Array.from(char.value).map(x => x.codePointAt(0).toString(16)); - if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f'); + if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); codes = codes.filter(x => x && x.length); return `${twemojiSvgBase}/${codes.join('-')}.svg`; } else { diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue index e558614c12..63db19a520 100644 --- a/packages/client/src/components/global/header.vue +++ b/packages/client/src/components/global/header.vue @@ -38,7 +38,7 @@ - diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue index 243d8614ba..70d0108e9f 100644 --- a/packages/client/src/components/global/misskey-flavored-markdown.vue +++ b/packages/client/src/components/global/misskey-flavored-markdown.vue @@ -31,6 +31,32 @@ const props = withDefaults(defineProps<{ } } +.mfm-x2 { + --mfm-zoom-size: 200%; +} + +.mfm-x3 { + --mfm-zoom-size: 400%; +} + +.mfm-x4 { + --mfm-zoom-size: 600%; +} + +.mfm-x2, .mfm-x3, .mfm-x4 { + font-size: var(--mfm-zoom-size); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* only half effective */ + font-size: calc(var(--mfm-zoom-size) / 2 + 50%); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* disabled */ + font-size: 100%; + } + } +} + @keyframes mfm-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue index 5748d9de61..a7f142f961 100644 --- a/packages/client/src/components/global/time.vue +++ b/packages/client/src/components/global/time.vue @@ -17,7 +17,7 @@ const props = withDefaults(defineProps<{ mode: 'relative', }); -const _time = typeof props.time == 'string' ? new Date(props.time) : props.time; +const _time = typeof props.time === 'string' ? new Date(props.time) : props.time; const absolute = _time.toLocaleString(); let now = $ref(new Date()); @@ -32,8 +32,7 @@ const relative = $computed(() => { ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : ago >= -1 ? i18n.ts._ago.justNow : - ago < -1 ? i18n.ts._ago.future : - i18n.ts._ago.unknown); + i18n.ts._ago.future); }); function tick() { diff --git a/packages/client/src/components/global/url.vue b/packages/client/src/components/global/url.vue index 55f6c5d5f9..34ba9024cc 100644 --- a/packages/client/src/components/global/url.vue +++ b/packages/client/src/components/global/url.vue @@ -18,7 +18,7 @@ - diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index 37076652fd..4556a82d55 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -91,7 +91,8 @@ export default defineComponent({ let style; switch (token.props.name) { case 'tada': { - style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : ''); + const speed = validTime(token.props.args.speed) || '1s'; + style = 'font-size: 150%;' + (this.$store.state.animatedMfm ? `animation: tada ${speed} linear infinite both;` : ''); break; } case 'jelly': { @@ -123,11 +124,13 @@ export default defineComponent({ break; } case 'jump': { - style = this.$store.state.animatedMfm ? 'animation: mfm-jump 0.75s linear infinite;' : ''; + const speed = validTime(token.props.args.speed) || '0.75s'; + style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : ''; break; } case 'bounce': { - style = this.$store.state.animatedMfm ? 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;' : ''; + const speed = validTime(token.props.args.speed) || '0.75s'; + style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; break; } case 'flip': { @@ -139,16 +142,19 @@ export default defineComponent({ break; } case 'x2': { - style = `font-size: 200%;`; - break; + return h('span', { + class: 'mfm-x2', + }, genEl(token.children)); } case 'x3': { - style = `font-size: 400%;`; - break; + return h('span', { + class: 'mfm-x3', + }, genEl(token.children)); } case 'x4': { - style = `font-size: 600%;`; - break; + return h('span', { + class: 'mfm-x4', + }, genEl(token.children)); } case 'font': { const family = @@ -168,7 +174,8 @@ export default defineComponent({ }, genEl(token.children)); } case 'rainbow': { - style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : ''; + const speed = validTime(token.props.args.speed) || '1s'; + style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : ''; break; } case 'sparkle': { diff --git a/packages/client/src/components/modal-page-window.vue b/packages/client/src/components/modal-page-window.vue index 2e17d5d030..21bdb657b7 100644 --- a/packages/client/src/components/modal-page-window.vue +++ b/packages/client/src/components/modal-page-window.vue @@ -39,8 +39,8 @@ export default defineComponent({ inject: { sideViewHook: { - default: null - } + default: null, + }, }, provide() { @@ -94,31 +94,31 @@ export default defineComponent({ }, { icon: 'fas fa-expand-alt', text: this.$ts.showInPage, - action: this.expand + action: this.expand, }, this.sideViewHook ? { icon: 'fas fa-columns', text: this.$ts.openInSideView, action: () => { this.sideViewHook(this.path); this.$refs.window.close(); - } + }, } : undefined, { icon: 'fas fa-external-link-alt', text: this.$ts.popout, - action: this.popout + action: this.popout, }, null, { icon: 'fas fa-external-link-alt', text: this.$ts.openInNewTab, action: () => { window.open(this.url, '_blank'); this.$refs.window.close(); - } + }, }, { icon: 'fas fa-link', text: this.$ts.copyLink, action: () => { copyToClipboard(this.url); - } + }, }]; }, }, @@ -155,7 +155,7 @@ export default defineComponent({ onContextmenu(ev: MouseEvent) { os.contextMenu(this.contextmenu, ev); - } + }, }, }); diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index d30284ca5f..6234b710d2 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -2,9 +2,9 @@
renoteButton.value.renote(true), 'esc': blur, 'm|o': () => menu(true), - 's': () => showContent.value != showContent.value, + 's': () => showContent.value !== showContent.value, }; useNoteCapture({ @@ -222,7 +222,7 @@ function react(viaKeyboard = false): void { reactionPicker.show(reactButton.value, reaction => { os.api('notes/reactions/create', { noteId: appearNote.id, - reaction: reaction + reaction: reaction, }); }, () => { focus(); @@ -233,7 +233,7 @@ function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; os.api('notes/reactions/delete', { - noteId: note.id + noteId: note.id, }); } @@ -257,7 +257,7 @@ function onContextmenu(ev: MouseEvent): void { function menu(viaKeyboard = false): void { os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, { - viaKeyboard + viaKeyboard, }).then(focus); } @@ -269,12 +269,12 @@ function showRenoteMenu(viaKeyboard = false): void { danger: true, action: () => { os.api('notes/delete', { - noteId: note.id + noteId: note.id, }); isDeleted.value = true; - } + }, }], renoteTime.value, { - viaKeyboard: viaKeyboard + viaKeyboard: viaKeyboard, }); } @@ -288,14 +288,14 @@ function blur() { os.api('notes/children', { noteId: appearNote.id, - limit: 30 + limit: 30, }).then(res => { replies.value = res; }); if (appearNote.replyId) { os.api('notes/conversation', { - noteId: appearNote.replyId + noteId: appearNote.replyId, }).then(res => { conversation.value = res.reverse(); }); diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue index c6907787b5..b813b9a2b9 100644 --- a/packages/client/src/components/note-simple.vue +++ b/packages/client/src/components/note-simple.vue @@ -5,7 +5,7 @@

- {{ note.cw }} +

diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 3cd7a819d4..e5744d1ce9 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -185,7 +185,7 @@ const keymap = { 'down|j|tab': focusAfter, 'esc': blur, 'm|o': () => menu(true), - 's': () => showContent.value != showContent.value, + 's': () => showContent.value !== showContent.value, }; useNoteCapture({ @@ -210,7 +210,7 @@ function react(viaKeyboard = false): void { reactionPicker.show(reactButton.value, reaction => { os.api('notes/reactions/create', { noteId: appearNote.id, - reaction: reaction + reaction: reaction, }); }, () => { focus(); @@ -221,7 +221,7 @@ function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; os.api('notes/reactions/delete', { - noteId: note.id + noteId: note.id, }); } @@ -245,7 +245,7 @@ function onContextmenu(ev: MouseEvent): void { function menu(viaKeyboard = false): void { os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, { - viaKeyboard + viaKeyboard, }).then(focus); } @@ -257,12 +257,12 @@ function showRenoteMenu(viaKeyboard = false): void { danger: true, action: () => { os.api('notes/delete', { - noteId: note.id + noteId: note.id, }); isDeleted.value = true; - } + }, }], renoteTime.value, { - viaKeyboard: viaKeyboard + viaKeyboard: viaKeyboard, }); } @@ -284,7 +284,7 @@ function focusAfter() { function readPromo() { os.api('promo/read', { - noteId: appearNote.id + noteId: appearNote.id, }); isDeleted.value = true; } diff --git a/packages/client/src/components/notification-setting-window.vue b/packages/client/src/components/notification-setting-window.vue index ec1efec261..64d828394b 100644 --- a/packages/client/src/components/notification-setting-window.vue +++ b/packages/client/src/components/notification-setting-window.vue @@ -1,5 +1,6 @@