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..dff3935571 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,8 @@ +'⚙️Server': +- packages/backend/**/* + +'🖥️Client': +- packages/client/**/* + +'‼️ 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..057208eda3 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,14 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +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/.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 a13686b438..9b086ddba9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,16 @@ You should also include the user name that made the change. - enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina - enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina - enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina +- replaced webpack with Vite @tamaina +- update dependencies @syuilo +- enhance: display URL of QR code for TOTP registration @syuilo +- enhance: Supports Unicode Emoji 14.0 @mei23 +- 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 + Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address. ### Bugfixes - Client: fix settings page @tamaina @@ -25,6 +35,14 @@ You should also include the user name that made the change. - Server: await promises when following or unfollowing users @Johann150 - Client: fix abuse reports page to be able to show all reports @Johann150 - Federation: Add rel attribute to host-meta @mei23 +- Client: fix profile picture height in mentions @tamaina +- MFM: more animated functions support `speed` parameter @futchitwo +- Federation: Fix quote renotes containing no text being federated correctly @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 ## 12.110.1 (2022/04/23) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cafca625d4..135a3e140c 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. diff --git a/Dockerfile b/Dockerfile index 174e2e9bc7..33d5faad12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:18.0.0-alpine3.15 AS base -ENV NODE_ENV=production +ARG NODE_ENV=production WORKDIR /misskey @@ -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 19e953aee8..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,22 +32,25 @@ ## ✨ 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 diff --git a/assets/title_float.svg b/assets/title_float.svg new file mode 100644 index 0000000000..43205ac1c4 Binary files /dev/null and b/assets/title_float.svg differ 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/integration/basic.js b/cypress/integration/basic.js index 7d27b649f4..eb15cfe223 100644 --- a/cypress/integration/basic.js +++ b/cypress/integration/basic.js @@ -1,5 +1,8 @@ describe('Before setup instance', () => { beforeEach(() => { + 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); @@ -32,6 +35,9 @@ describe('Before setup instance', () => { describe('After setup instance', () => { beforeEach(() => { + 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); @@ -70,6 +76,9 @@ describe('After setup instance', () => { describe('After user signup', () => { beforeEach(() => { + 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); @@ -129,6 +138,9 @@ describe('After user signup', () => { describe('After user singed in', () => { beforeEach(() => { + 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); @@ -163,12 +175,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/integration/widgets.js b/cypress/integration/widgets.js new file mode 100644 index 0000000000..d63ff274bd --- /dev/null +++ b/cypress/integration/widgets.js @@ -0,0 +1,84 @@ +describe('After user signed in', () => { + beforeEach(() => { + cy.window(win => { + win.indexedDB.deleteDatabase('keyval-store'); + }); + cy.viewport('macbook-16'); + cy.request('POST', '/api/reset-db').as('reset'); + cy.get('@reset').its('status').should('equal', 204); + cy.reload(true); + + // インスタンス初期セットアップ + cy.request('POST', '/api/admin/accounts/create', { + username: 'admin', + password: 'pass', + }).its('body').as('admin'); + + // ユーザー作成 + cy.request('POST', '/api/signup', { + username: 'alice', + password: 'alice1234', + }).its('body').as('alice'); + + 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'); + }); + + 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/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/locales/ar-SA.yml b/locales/ar-SA.yml index f3f8b45777..efad7c954b 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: "تأثير (هلام)" @@ -1030,12 +1034,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 +1205,13 @@ _charts: _instanceCharts: requests: "الطلبات" users: "تباين عدد المستخدمين" + usersTotal: "تباين عدد المستخدمين" notes: "تباين عدد الملاحظات" + notesTotal: "تباين عدد الملاحظات" + ff: "تباين عدد حسابات المتابَعة/المتابِعة" + ffTotal: "تباين عدد حسابات المتابَعة/المتابِعة" files: "تباين عدد الملفات" + filesTotal: "تباين عدد الملفات" _timelines: home: "الرئيسي" local: "المحلي" @@ -1321,6 +1330,7 @@ _pages: random: "عشوائي" value: "القيم" fn: "دوال" + text: "إجراءات على النصوص" convert: "تحويل" list: "القوائم" blocks: @@ -1501,6 +1511,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..bcecb06253 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1621,6 +1621,9 @@ _notification: followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" groupInvited: "গ্রুপের আমন্ত্রনসমূহ" app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি" + _actions: + reply: "জবাব" + renote: "রিনোট" _deck: alwaysShowMainColumn: "সর্বদা মেইন কলাম দেখান" columnAlign: "কলাম সাজান" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 5f74cb6bef..577fbca2ae 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,173 @@ 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" _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..a3ff6c0a6c 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1006,7 +1006,7 @@ _instanceMute: heading: "Liste der stummzuschaltenden Instanzen" _theme: explore: "Farbschemata erforschen" - install: "Farbschmata installieren" + install: "Farbschemata installieren" manage: "Farbschemaverwaltung" code: "Farbschemencode" description: "Beschreibung" @@ -1613,8 +1613,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 +1630,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..a7d69d6d4f 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1613,8 +1613,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 +1630,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..8fff5ca4d9 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" @@ -1432,6 +1506,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..3b5dc1b06c 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1615,6 +1615,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..a97ac819a9 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,6 +840,7 @@ 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" _emailUnavailable: used: "Alamat surel ini telah digunakan" @@ -1613,6 +1615,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 +1630,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..4d10356698 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1433,6 +1433,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 4d04cd28c8..9cd1d1eedb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -425,7 +425,7 @@ quoteQuestion: "引用として添付しますか?" noMessagesYet: "まだチャットはありません" newMessageExists: "新しいメッセージがあります" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" -signinRequired: "ログインしてください" +signinRequired: "続行する前に、サインアップまたはサインインが必要です" invitations: "招待" invitationCode: "招待コード" checking: "確認しています" @@ -842,6 +842,7 @@ oneDay: "1日" oneWeek: "1週間" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" +rateLimitExceeded: "レート制限を超えました" _emailUnavailable: used: "既に使用されています" @@ -1110,7 +1111,6 @@ _sfx: channel: "チャンネル通知" _ago: - unknown: "謎" future: "未来" justNow: "たった今" secondsAgo: "{n}秒前" @@ -1157,6 +1157,7 @@ _2fa: registerKey: "キーを登録" step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。" step2: "次に、表示されているQRコードをアプリでスキャンします。" + step2Url: "デスクトップアプリでは次のURLを入力します:" step3: "アプリに表示されているトークンを入力して完了です。" step4: "これからログインするときも、同じようにトークンを入力します。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーもしくは端末の指紋認証やPINを使用してログインするように設定できます。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 52ecd8c24e..fd6945160e 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1202,6 +1202,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..74d06185d9 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: "형식이 올바르지 않습니다" @@ -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..d0a83eb6a8 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -371,6 +371,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..7fabab3b4f 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1401,6 +1401,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..c1afa5b56c 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -37,17 +37,58 @@ 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." +follow: "Seguindo" enterEmoji: "Inserir emoji" renote: "Repostar" renoted: "Repostado" cantRenote: "Não pode repostar" cantReRenote: "Não pode repostar este repost" pinnedNote: "Post fixado" +pinned: "Afixar no perfil" sensitive: "Conteúdo sensível" mute: "Silenciar" unmute: "Dessilenciar" @@ -57,6 +98,7 @@ 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 +106,7 @@ addFile: "Adicionar arquivo" nsfw: "Conteúdo sensível" monthX: "mês de {month}" pinnedNotes: "Post fixado" +userList: "Listas" smtpUser: "Nome de usuário" smtpPass: "Senha" user: "Usuários" @@ -72,9 +115,11 @@ _email: _follow: title: "Você tem um novo seguidor" _mfm: + mention: "Menção" search: "Pesquisar" _theme: keys: + mention: "Menção" renote: "Repostar" _sfx: note: "Posts" @@ -82,15 +127,47 @@ _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" + userLists: "Listas" +_pages: + script: + categories: + list: "Listas" + blocks: + _join: + arg1: "Listas" + _randomPick: + arg1: "Listas" + _dailyRandomPick: + arg1: "Listas" + _seedRandomPick: + arg2: "Listas" + _pick: + arg1: "Listas" + _listLen: + arg1: "Listas" + types: + array: "Listas" _notification: youWereFollowed: "Você tem um novo seguidor" _types: + follow: "Seguindo" + mention: "Menção" + renote: "Repostar" + _actions: + reply: "Responder" renote: "Repostar" _deck: _columns: notifications: "Notificações" tl: "Timeline" + list: "Listas" + mentions: "Menções" 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..7405c07e6c 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1599,6 +1599,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..6818a64d30 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1628,6 +1628,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/uk-UA.yml b/locales/uk-UA.yml index 073b2c310e..480526c134 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: @@ -1377,6 +1429,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..ffe5ba1976 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1,33 +1,1091 @@ --- _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 trá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" +_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: "Độ sá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" @@ -39,10 +1097,562 @@ _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." + 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 mã 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..c719dcb767 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: "默认将媒体文件标记为敏感内容" @@ -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..e9b7ab6540 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: "捲動條 (漂浮)" @@ -1010,9 +1131,12 @@ _2fa: registerKey: "註冊鍵" step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。" step2: "然後,掃描螢幕上的QR code。" + 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/okteto.yml b/okteto.yml new file mode 100644 index 0000000000..e2996fbbc9 --- /dev/null +++ b/okteto.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/package.json b/package.json index 9c3c39bf93..850a52ceb6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "cy:open": "cypress open", "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", diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs index dfc9d04950..5a06889dcd 100644 --- a/packages/backend/.eslintrc.cjs +++ b/packages/backend/.eslintrc.cjs @@ -16,6 +16,17 @@ module.exports = { '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..589522216d 100644 --- a/packages/backend/.mocharc.json +++ b/packages/backend/.mocharc.json @@ -5,6 +5,6 @@ "loader=./test/loader.js" ], "slow": 1000, - "timeout": 35000, + "timeout": 3000, "exit": true } 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 5950cd23ff..4e0d60b74e 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": { @@ -15,25 +15,24 @@ }, "dependencies": { "@bull-board/koa": "3.10.4", - "@discordapp/twemoji": "13.1.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", - "@typescript-eslint/eslint-plugin": "5.20.0", - "@typescript-eslint/parser": "5.20.0", "abort-controller": "3.0.0", "ajv": "8.11.0", "archiver": "5.3.1", "autobind-decorator": "2.4.0", "autwh": "0.1.0", - "aws-sdk": "2.1120.0", + "aws-sdk": "2.1135.0", "bcryptjs": "2.4.3", "blurhash": "1.1.5", - "broadcast-channel": "4.11.0", - "bull": "4.8.2", + "broadcast-channel": "4.12.0", + "bull": "4.8.3", "cacheable-lookup": "6.0.4", "cbor": "8.1.0", "chalk": "5.0.1", @@ -44,22 +43,19 @@ "date-fns": "2.28.0", "deep-email-validator": "0.1.21", "escape-regexp": "0.0.1", - "eslint": "8.14.0", - "eslint-plugin-import": "2.26.0", "feed": "4.2.2", "file-type": "17.1.1", "fluent-ffmpeg": "2.1.2", - "got": "12.0.3", + "got": "12.0.4", "hpagent": "0.1.2", - "http-signature": "1.3.6", - "ip-cidr": "3.0.7", + "ip-cidr": "3.0.8", "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": "10.5.19", + "jsrsasign": "10.5.22", "koa": "2.13.4", "koa-bodyparser": "4.3.0", "koa-favicon": "2.1.0", @@ -69,19 +65,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.4", + "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", @@ -101,33 +96,32 @@ "s-age": "1.1.2", "sanitize-html": "2.7.0", "semver": "7.3.7", - "sharp": "0.30.4", + "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", "syslog-pro": "1.0.0", - "systeminformation": "5.11.14", + "systeminformation": "5.11.15", "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.0", + "tsc-alias": "1.6.7", + "tsconfig-paths": "4.0.0", "twemoji-parser": "14.0.0", "typeorm": "0.3.6", - "typescript": "4.6.3", "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", + "ws": "8.6.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", @@ -138,7 +132,7 @@ "@types/js-yaml": "4.0.5", "@types/jsdom": "16.2.14", "@types/jsonld": "1.5.6", - "@types/jsrsasign": "10.2.1", + "@types/jsrsasign": "10.5.1", "@types/koa": "2.13.4", "@types/koa-bodyparser": "4.3.7", "@types/koa-cors": "0.0.2", @@ -151,12 +145,11 @@ "@types/koa__multer": "2.0.4", "@types/koa__router": "8.0.11", "@types/mocha": "9.1.1", - "@types/node": "17.0.25", + "@types/node": "17.0.35", "@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", @@ -174,6 +167,12 @@ "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", "@types/ws": "8.5.3", + "@typescript-eslint/eslint-plugin": "5.26.0", + "@typescript-eslint/parser": "5.26.0", + "typescript": "4.7.2", + "eslint": "8.16.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 0426cb8bcd..d1f9cd9552 100644 --- a/packages/backend/src/@types/http-signature.d.ts +++ b/packages/backend/src/@types/http-signature.d.ts @@ -1,4 +1,4 @@ -declare module 'http-signature' { +declare module '@peertube/http-signature' { import { IncomingMessage, ClientRequest } from 'node:http'; interface ISignature { 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 c2e6bea45e..9654a4f3b0 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -46,7 +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'].file.replace(/^_client_dist_\//, ''); + mixin.clientEntry = clientManifest['src/init.ts']; if (!config.redis.prefix) config.redis.prefix = mixin.host; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index eb5fc2e186..e09e93f04e 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,8 @@ 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'; const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); @@ -212,7 +211,7 @@ export async function initDb() { if (db.isInitialized) { // nop } else { - await db.connect(); + await db.initialize(); } } 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/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/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index cf5fcb1787..c0abbb4f93 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -144,13 +144,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 +162,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/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..67d5f5d248 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'; 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/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 { 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/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 133dd36066..a48c2d4122 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'; diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 9a85e4565b..fbe25e1732 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 IPCIDR from 'ip-cidr'; 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,37 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi throw new ApiError(accessDenied); } + if (ep.meta.requireCredential && 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 { + // 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 ip = IPCIDR.createAddress(ctx.ip).mask(64); + + limitActor = 'ip-' + parseInt(ip, 2).toString(36); + } + + 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 +86,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 +98,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/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/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/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 40a3ba73ca..a133294169 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -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'], }, @@ -172,10 +172,14 @@ 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 = null; diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index a72a58a843..f93d4f718b 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -61,7 +61,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/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 +25,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 }, 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/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts index 027d078ce1..c34e043145 100644 --- a/packages/backend/src/server/file/send-drive-file.ts +++ b/packages/backend/src/server/file/send-drive-file.ts @@ -4,11 +4,11 @@ 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 { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.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); diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index c1a2a6dfff..f31de2b7f4 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -2,6 +2,7 @@ * Core Server */ +import cluster from 'node:cluster'; import * as fs from 'node:fs'; import * as http from 'node:http'; import Koa from 'koa'; @@ -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/web/boot.js b/packages/backend/src/server/web/boot.js index a9ee0df4f1..94329e11c9 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -54,14 +54,10 @@ //#endregion //#region Script - const salt = localStorage.getItem('salt') - ? `?salt=${localStorage.getItem('salt')}` - : ''; - - import(`/assets/${CLIENT_ENTRY}${salt}`) - .catch(async () => { + import(`/assets/${CLIENT_ENTRY}`) + .catch(async e => { await checkUpdate(); - renderError('APP_FETCH_FAILED'); + renderError('APP_FETCH_FAILED', JSON.stringify(e)); }) //#endregion @@ -142,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 9e31f2389e..2feee72be7 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -74,9 +74,9 @@ app.use(views(_dirname + '/views', { extension: 'pug', options: { version: config.version, - clientEntry: () => process.env.NODE_ENV === 'production' ? + getClientEntry: () => process.env.NODE_ENV === 'production' ? config.clientEntry : - JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'].file.replace(/^_client_dist_\//, ''), + JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'], config, }, })); @@ -247,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にはしない @@ -292,7 +292,7 @@ router.get('/notes/:note', async (ctx, next) => { themeColor: meta.themeColor, }); - ctx.set('Cache-Control', 'public, max-age=180'); + ctx.set('Cache-Control', 'public, max-age=15'); return; } @@ -329,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'); } @@ -360,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; } @@ -385,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; } @@ -409,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; } @@ -468,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/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/views/base.pug b/packages/backend/src/server/web/views/base.pug index d79354d118..230ed1578a 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -1,17 +1,23 @@ 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 +36,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 @@ -52,7 +64,7 @@ html script. var VERSION = "#{version}"; - var CLIENT_ENTRY = "#{clientEntry()}"; + var CLIENT_ENTRY = "#{clientEntry.file}"; script include ../boot.js @@ -65,4 +77,14 @@ html div#splash img#splashIcon(src= icon || '/static-assets/splash.png') div#splashSpinner + + + + + + + + + + block content 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/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts index da93bc97c7..ca12ab8d3d 100644 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ b/packages/backend/src/services/drive/generate-video-thumbnail.ts @@ -1,38 +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`; - - // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) - 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/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/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/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..5d8b28ec7a 100644 --- a/packages/backend/test/activitypub.ts +++ b/packages/backend/test/activitypub.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = 'test'; -import rndstr from 'rndstr'; import * as assert from 'assert'; +import rndstr from 'rndstr'; import { initTestDb } from './utils.js'; describe('ActivityPub', () => { @@ -57,8 +57,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..823e388a82 100644 --- a/packages/backend/test/chart.ts +++ b/packages/backend/test/chart.ts @@ -2,7 +2,6 @@ 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'; @@ -11,6 +10,7 @@ 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 { async, initTestDb } from './utils.js'; describe('Chart', () => { let testChart: TestChart; @@ -33,7 +33,7 @@ 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)), }); })); @@ -52,7 +52,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -60,7 +60,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); })); @@ -76,7 +76,7 @@ describe('Chart', () => { foo: { dec: [1, 0, 0], inc: [0, 0, 0], - total: [-1, 0, 0] + total: [-1, 0, 0], }, }); @@ -84,7 +84,7 @@ describe('Chart', () => { foo: { dec: [1, 0, 0], inc: [0, 0, 0], - total: [-1, 0, 0] + total: [-1, 0, 0], }, }); })); @@ -97,7 +97,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [0, 0, 0] + total: [0, 0, 0], }, }); @@ -105,7 +105,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [0, 0, 0] + total: [0, 0, 0], }, }); })); @@ -123,7 +123,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [3, 0, 0], - total: [3, 0, 0] + total: [3, 0, 0], }, }); @@ -131,7 +131,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [3, 0, 0], - total: [3, 0, 0] + total: [3, 0, 0], }, }); })); @@ -149,7 +149,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -157,7 +157,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); })); @@ -178,7 +178,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 1, 0], - total: [2, 1, 0] + total: [2, 1, 0], }, }); @@ -186,7 +186,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); })); @@ -238,7 +238,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 1], - total: [2, 1, 1] + total: [2, 1, 1], }, }); @@ -246,7 +246,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); })); @@ -265,7 +265,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [1, 1, 1] + total: [1, 1, 1], }, }); @@ -273,7 +273,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); })); @@ -296,7 +296,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [2, 1, 1] + total: [2, 1, 1], }, }); @@ -304,7 +304,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); })); @@ -325,7 +325,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -333,7 +333,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); })); @@ -356,7 +356,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -364,7 +364,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [2, 0, 0], - total: [2, 0, 0] + total: [2, 0, 0], }, }); })); @@ -383,7 +383,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -391,7 +391,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [1, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -399,7 +399,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [0, 0, 0] + total: [0, 0, 0], }, }); @@ -407,7 +407,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [0, 0, 0] + total: [0, 0, 0], }, }); })); @@ -493,7 +493,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); @@ -501,7 +501,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 0, 0], - total: [1, 0, 0] + total: [1, 0, 0], }, }); })); @@ -523,7 +523,7 @@ describe('Chart', () => { foo: { dec: [0, 0, 0], inc: [0, 1, 0], - total: [100, 1, 0] + total: [100, 1, 0], }, }); @@ -531,7 +531,7 @@ 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 09e812f437..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 'node: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/yarn.lock b/packages/backend/yarn.lock index fd91be84af..d131f70e38 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -63,17 +63,12 @@ dependencies: "@bull-board/api" "3.10.4" -"@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" @@ -89,14 +84,14 @@ ky "^0.25.1" ky-universal "^0.8.2" -"@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 +105,19 @@ pump "^3.0.0" secure-json-parse "^2.1.0" -"@eslint/eslintrc@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae" - integrity sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg== +"@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 +139,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 +247,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 +266,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 +277,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 +299,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,10 +549,10 @@ resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.6.tgz#4396c0b17128abf5773bb68b5453b88fc565b0d4" integrity sha512-OUcfMjRie5IOrJulUQwVNvV57SOdKcTfBj3pjXNxzXqeOIrY2aGDNGW/Tlp83EQPkz4tCE6YWVrGuc/ZeaAQGg== -"@types/jsrsasign@10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@types/jsrsasign/-/jsrsasign-10.2.1.tgz#b82882523dfb5c476673dbef344ad838f96fb43d" - integrity sha512-piCIOMY0+d2wwRNcRw56VBqFYCYYeZ1c/NlUKVHTV3Y9j1RE2qpgCQvClI6yhH2sk8OoXiah43i9FmnC5tL2RQ== +"@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" @@ -671,10 +693,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50" integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA== -"@types/node@17.0.25": - version "17.0.25" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" - integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== +"@types/node@17.0.35": + version "17.0.35" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" + integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== "@types/node@^14.11.8": version "14.17.9" @@ -705,11 +727,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" @@ -855,85 +872,85 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz#022531a639640ff3faafaf251d1ce00a2ef000a1" - integrity sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q== +"@typescript-eslint/eslint-plugin@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.26.0.tgz#c1f98ccba9d345e38992975d3ca56ed6260643c2" + integrity sha512-oGCmo0PqnRZZndr+KwvvAUvD3kNE4AfyoGCwOZpoCncSh4MVD06JTE8XQa2u9u+NX5CsyZMBTEc2C72zx38eYA== dependencies: - "@typescript-eslint/scope-manager" "5.20.0" - "@typescript-eslint/type-utils" "5.20.0" - "@typescript-eslint/utils" "5.20.0" - debug "^4.3.2" + "@typescript-eslint/scope-manager" "5.26.0" + "@typescript-eslint/type-utils" "5.26.0" + "@typescript-eslint/utils" "5.26.0" + 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.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.20.0.tgz#4991c4ee0344315c2afc2a62f156565f689c8d0b" - integrity sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w== +"@typescript-eslint/parser@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.26.0.tgz#a61b14205fe2ab7533deb4d35e604add9a4ceee2" + integrity sha512-n/IzU87ttzIdnAH5vQ4BBDnLPly7rC5VnjN3m0xBG82HK6rhRxnCb3w/GyWbNDghPd+NktJqB/wl6+YkzZ5T5Q== dependencies: - "@typescript-eslint/scope-manager" "5.20.0" - "@typescript-eslint/types" "5.20.0" - "@typescript-eslint/typescript-estree" "5.20.0" - debug "^4.3.2" + "@typescript-eslint/scope-manager" "5.26.0" + "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/typescript-estree" "5.26.0" + debug "^4.3.4" -"@typescript-eslint/scope-manager@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz#79c7fb8598d2942e45b3c881ced95319818c7980" - integrity sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg== +"@typescript-eslint/scope-manager@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.26.0.tgz#44209c7f649d1a120f0717e0e82da856e9871339" + integrity sha512-gVzTJUESuTwiju/7NiTb4c5oqod8xt5GhMbExKsCTp6adU3mya6AGJ4Pl9xC7x2DX9UYFsjImC0mA62BCY22Iw== dependencies: - "@typescript-eslint/types" "5.20.0" - "@typescript-eslint/visitor-keys" "5.20.0" + "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/visitor-keys" "5.26.0" -"@typescript-eslint/type-utils@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz#151c21cbe9a378a34685735036e5ddfc00223be3" - integrity sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw== +"@typescript-eslint/type-utils@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.26.0.tgz#937dee97702361744a3815c58991acf078230013" + integrity sha512-7ccbUVWGLmcRDSA1+ADkDBl5fP87EJt0fnijsMFTVHXKGduYMgienC/i3QwoVhDADUAPoytgjbZbCOMj4TY55A== dependencies: - "@typescript-eslint/utils" "5.20.0" - debug "^4.3.2" + "@typescript-eslint/utils" "5.26.0" + debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.20.0.tgz#fa39c3c2aa786568302318f1cb51fcf64258c20c" - integrity sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg== +"@typescript-eslint/types@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.26.0.tgz#cb204bb154d3c103d9cc4d225f311b08219469f3" + integrity sha512-8794JZFE1RN4XaExLWLI2oSXsVImNkl79PzTOOWt9h0UHROwJedNOD2IJyfL0NbddFllcktGIO2aOu10avQQyA== -"@typescript-eslint/typescript-estree@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz#ab73686ab18c8781bbf249c9459a55dc9417d6b0" - integrity sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w== +"@typescript-eslint/typescript-estree@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.26.0.tgz#16cbceedb0011c2ed4f607255f3ee1e6e43b88c3" + integrity sha512-EyGpw6eQDsfD6jIqmXP3rU5oHScZ51tL/cZgFbFBvWuCwrIptl+oueUZzSmLtxFuSOQ9vDcJIs+279gnJkfd1w== dependencies: - "@typescript-eslint/types" "5.20.0" - "@typescript-eslint/visitor-keys" "5.20.0" - debug "^4.3.2" - globby "^11.0.4" + "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/visitor-keys" "5.26.0" + 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.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.20.0.tgz#b8e959ed11eca1b2d5414e12417fd94cae3517a5" - integrity sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w== +"@typescript-eslint/utils@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.26.0.tgz#896b8480eb124096e99c8b240460bb4298afcfb4" + integrity sha512-PJFwcTq2Pt4AMOKfe3zQOdez6InIDOjUJJD3v3LyEtxHGVVRK3Vo7Dd923t/4M9hSH2q2CLvcTdxlLPjcIk3eg== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.20.0" - "@typescript-eslint/types" "5.20.0" - "@typescript-eslint/typescript-estree" "5.20.0" + "@typescript-eslint/scope-manager" "5.26.0" + "@typescript-eslint/types" "5.26.0" + "@typescript-eslint/typescript-estree" "5.26.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz#70236b5c6b67fbaf8b2f58bf3414b76c1e826c2a" - integrity sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg== +"@typescript-eslint/visitor-keys@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.26.0.tgz#7195f756e367f789c0e83035297c45b417b57f57" + integrity sha512-wei+ffqHanYDOQgg/fS6Hcar6wAWv0CUPQ3TZzOWd2BLfgP539rb49bwua8WRAs7R6kOSLn82rfEu2ro6Llt8Q== dependencies: - "@typescript-eslint/types" "5.20.0" - eslint-visitor-keys "^3.0.0" + "@typescript-eslint/types" "5.26.0" + eslint-visitor-keys "^3.3.0" "@ungap/promise-all-settled@1.1.2": version "1.1.2" @@ -973,10 +990,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" @@ -998,11 +1015,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" @@ -1244,19 +1266,7 @@ 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.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.3: +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== @@ -1278,10 +1288,10 @@ autwh@0.1.0: dependencies: oauth "0.9.15" -aws-sdk@2.1120.0: - version "2.1120.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1120.0.tgz#a299f595448019c4b4b69fa9aa57fd58658497a6" - integrity sha512-3cKXUFxC3CDBbJ/JlXEKmJZKFZhqGii7idGaLxvV5/OzqEDUstYkHGX3TCJdQRHrRwpFvRVOekXSwLxBltqXuQ== +aws-sdk@2.1135.0: + version "2.1135.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1135.0.tgz#8c14aa6894be529cb5fb7b6d19f3dc70e4f35816" + integrity sha512-bl9n4QgrEh52hmQ+Jo76BgJXM/p+PwfVZvImEQHFeel/33H/PDLcTJquEw5bzxM1HRNI24iH+FNPwyWLMrttTw== dependencies: buffer "4.9.2" events "1.1.1" @@ -1411,22 +1421,22 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.1, braces@~3.0.2: +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.11.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.11.0.tgz#b9ebc7ce1326120088e61d2197477496908a1a9e" - integrity sha512-4FS1Zk+ttekfXHq5I2R7KhN9AsnZUFVV5SczrTtnZPuf5w+jw+fqM1PJHuHzwEXJezJeCbJxoZMDcFqsIN2c1Q== +broadcast-channel@4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.12.0.tgz#891876c5262376ab714b33a0d9e9d87a894b5bcb" + integrity sha512-hfb0L2P2CEsdM5nSqlRiZ2gQFHPdJNs1VU4rTLnFPYtq5vQAnyOdjIx+04KCWfFfRhfP3OEbxxQmnouSi8WCbQ== dependencies: "@babel/runtime" "^7.16.0" detect-node "^2.1.0" microtime "3.0.0" - oblivious-set "1.0.0" + oblivious-set "1.1.1" p-queue "6.6.2" rimraf "3.0.2" unload "2.3.1" @@ -1503,10 +1513,10 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "~3.7.0" -bull@4.8.2: - version "4.8.2" - resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.2.tgz#0d02fe389777abe29d50fd46d123bc62e074cfcd" - integrity sha512-S7CNIL9+vsbLKwOGkUI6mawY5iABKQJLZn5a7KPnxAZrDhFXkrxsHHXLCKUR/+Oqys3Vk5ElWdj0SLtK84b1Nw== +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" @@ -1728,7 +1738,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== @@ -1867,7 +1877,7 @@ 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.3: +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== @@ -1892,10 +1902,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" @@ -2132,6 +2142,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" @@ -2153,13 +2170,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" @@ -2269,21 +2279,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" @@ -2708,22 +2713,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.14.0: - version "8.14.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.14.0.tgz#62741f159d9eb4a79695b28ec4989fcdec623239" - integrity sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw== +eslint@8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.16.0.tgz#6d936e2d524599f2a86c708483b4c372c5d3bbae" + integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA== dependencies: - "@eslint/eslintrc" "^1.2.2" + "@eslint/eslintrc" "^1.3.0" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -2734,14 +2734,14 @@ eslint@8.14.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" @@ -2750,7 +2750,7 @@ eslint@8.14.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" @@ -2764,13 +2764,13 @@ esm@^3.2.22: 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: @@ -2852,13 +2852,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" @@ -2905,6 +2898,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" @@ -2977,14 +2981,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" @@ -3008,16 +3004,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" @@ -3218,7 +3204,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== @@ -3256,37 +3242,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" @@ -3302,6 +3261,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" @@ -3319,10 +3290,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.0.4: + version "12.0.4" + resolved "https://registry.yarnpkg.com/got/-/got-12.0.4.tgz#e3b6bf6992425f904076fd71aac7030da5122de8" + integrity sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg== dependencies: "@sindresorhus/is" "^4.6.0" "@szmarczak/http-timer" "^5.0.1" @@ -3353,11 +3324,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" @@ -3412,13 +3378,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" @@ -3515,15 +3474,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" @@ -3608,11 +3558,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" @@ -3708,10 +3653,10 @@ ip-address@^7.1.0: jsbn "1.1.0" sprintf-js "1.1.2" -ip-cidr@3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.7.tgz#22708dd4f2d3f6397c0fb7d647b44e3c565937e9" - integrity sha512-0cBBICDnmmpAdULMbMVdi4f0mSG+VWY/QBPL/OIIjuom14x7Y63VhpS/uSAOycasXOeGXah5y0eu//PDU51aNw== +ip-cidr@3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.8.tgz#51876484109e4aa200b86b3dc32f24a1d8986429" + integrity sha512-DLrHwoFNuLVNulwoQuHLdkIED1Hyo9iB0MB8XzZdfie23b5bonJXVB5aCyVbbvXYOmQrw3nZDcjnSO7MMYgjJg== dependencies: ip-address "^7.1.0" jsbn "^1.1.0" @@ -3856,13 +3801,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" @@ -3970,11 +3908,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" @@ -4125,7 +4058,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== @@ -4170,20 +4103,20 @@ jsonld@5.2.0: 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@10.5.19: - version "10.5.19" - resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.19.tgz#61cd378190c3e65bd1a26a088696736e4437a806" - integrity sha512-GgOdly2Ee9nS+qxOjLkQKaoSTKqlk6lFKcKLPlNJOApoOUcqL2z+l4dAcBzYnZkA3tg+LwFOyQnqbuFn5IPdvw== +jsrsasign@10.5.22: + version "10.5.22" + resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.22.tgz#a702fb591e634767ca3296ce7a212f92974df17c" + integrity sha512-exqUDmWKOCUK4fT79z/Fi2qGV4c+WCPjHrtJ/lVUUrrbBwJ5T5HppCcalGf3tuOlmyNdyMZ074r1bqPOUNl4Uw== jstransformer@1.0.0: version "1.0.0" @@ -4493,11 +4426,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" @@ -4543,7 +4471,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== @@ -4628,27 +4556,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" @@ -4658,6 +4581,14 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +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" + microtime@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/microtime/-/microtime-3.0.0.tgz#d140914bde88aa89b4f9fd2a18620b435af0f39b" @@ -4710,12 +4641,12 @@ 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, minimatch@^3.1.2: version "3.1.2" @@ -4724,13 +4655,6 @@ minimatch@^3.0.4, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -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 "^2.0.1" - minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -4821,32 +4745,30 @@ 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" @@ -4905,10 +4827,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" @@ -4924,7 +4846,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== -nanoid@3.3.1, nanoid@^3.1.30: +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.1.30: version "3.3.1" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== @@ -4980,7 +4907,7 @@ node-addon-api@^1.2.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== -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== @@ -5007,10 +4934,10 @@ node-fetch@3.0.0-beta.9: 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.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.4.tgz#3fbca2d8838111048232de54cb532bd3cf134947" + integrity sha512-WvYJRN7mMyOLurFR2YpysQGuwYrJN+qrrpHjJDuKMcSPdfFccRUla/kng2mz6HWSBxJcqPbvatS6Gb4RhOzCJw== dependencies: data-uri-to-buffer "^4.0.0" fetch-blob "^3.1.4" @@ -5054,10 +4981,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" @@ -5184,10 +5111,10 @@ 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== +oblivious-set@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.1.1.tgz#d9d38e9491d51f27a5c3ec1681d2ba40aa81e98b" + integrity sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w== on-finished@^2.3.0: version "2.3.0" @@ -5373,11 +5300,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" @@ -5516,11 +5438,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" @@ -5536,14 +5470,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" @@ -5575,10 +5501,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" @@ -5837,6 +5763,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" @@ -6062,14 +5993,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" @@ -6283,17 +6206,17 @@ sha.js@^2.4.11: inherits "^2.0.1" safe-buffer "^5.0.1" -sharp@0.30.4: - version "0.30.4" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.4.tgz#73d9daa63bbc20da189c9328d75d5d395fc8fb73" - integrity sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q== +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.3" - detect-libc "^2.0.1" - node-addon-api "^4.3.0" - prebuild-install "^7.0.1" - semver "^7.3.7" - simple-get "^4.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.0" tar-fs "^2.1.1" tunnel-agent "^0.6.0" @@ -6338,7 +6261,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== @@ -6651,10 +6574,10 @@ syslog-pro@1.0.0: dependencies: moment "^2.22.2" -systeminformation@5.11.14: - version "5.11.14" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.14.tgz#21fcb6f05d33e17d69c236b9c1b3d9c53d1d2b3a" - integrity sha512-m8CJx3fIhKohanB0ExTk5q53uI1J0g5B09p77kU+KxnxRVpADVqTAwCg1PFelqKsj4LHd+qmVnumb511Hg4xow== +systeminformation@5.11.15: + version "5.11.15" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.15.tgz#013038688e7ba375a5c8e88b8e7739bda7110e6b" + integrity sha512-zUbObRjQeZcu84z9NVSm9JTiCPyPQ3MefJ3+76yvp+TeCv9WsO3szijyQLv0fChRrm2/sl2De3y1ewUOYOtz2Q== tapable@^2.2.0: version "2.2.0" @@ -6812,22 +6735,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.0: + version "10.8.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.0.tgz#3ceb5ac3e67ae8025c1950626aafbdecb55d82ce" + integrity sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA== 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" @@ -6838,22 +6761,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.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.6.7.tgz#354840d6444db79dd13fcc4f9ec37574ff9d5120" + integrity sha512-GRbZx/zTee01JtrHB7hkddgxn+aQqYDmRaFv/MTYIqBbk/L8Zf0nA/T60wXOr/Q7002YXppUFXsqsu5ViWB4vQ== 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== @@ -6897,12 +6829,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== @@ -6984,10 +6911,10 @@ typeorm@0.3.6: 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.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" + integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== ulid@2.3.0: version "2.3.0" @@ -7099,10 +7026,10 @@ uuid@8.3.2, uuid@^8.3.0, uuid@^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" @@ -7142,10 +7069,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" @@ -7225,20 +7152,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" @@ -7268,10 +7195,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" @@ -7296,10 +7223,10 @@ 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.6.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23" + integrity sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw== ws@^8.2.3: version "8.4.2" diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js index a6e23e5171..1c2ab0a427 100644 --- a/packages/client/.eslintrc.js +++ b/packages/client/.eslintrc.js @@ -1,68 +1,79 @@ 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: { // 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 index b8b906b82e..67f724a9aa 100644 --- a/packages/client/@types/theme.d.ts +++ b/packages/client/@types/theme.d.ts @@ -1,5 +1,7 @@ -import { Theme } from '../src/scripts/theme'; - declare module '@/themes/*.json5' { - export = Theme; + import { Theme } from "@/scripts/theme"; + + const theme: Theme; + + export default theme; } diff --git a/packages/client/package.json b/packages/client/package.json index 1d62d78d88..a4c6638565 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -10,47 +10,37 @@ "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", - "@typescript-eslint/parser": "5.20.0", - "@vitejs/plugin-vue": "2.3.1", - "@vue/compiler-sfc": "3.2.33", "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.11.0", + "broadcast-channel": "4.12.0", "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2", - "chart.js": "3.7.1", + "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", "date-fns": "2.28.0", "escape-regexp": "0.0.1", - "eslint": "8.14.0", - "eslint-plugin-vue": "8.7.1", "eventemitter3": "4.0.7", "feed": "4.2.2", - "glob": "7.2.0", "idb-keyval": "6.1.0", "insert-text-at-cursor": "0.3.0", "json5": "2.2.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", + "photoswipe": "5.2.7", "prismjs": "1.28.0", "private-ip": "2.3.3", "promise-limit": "2.7.0", @@ -61,31 +51,35 @@ "random-seed": "0.3.0", "reflect-metadata": "0.1.13", "rndstr": "1.0.0", - "rollup": "2.70.2", "s-age": "1.1.2", - "sass": "1.50.1", + "sass": "1.52.1", "seedrandom": "3.0.5", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "three": "0.139.2", - "throttle-debounce": "4.0.1", + "three": "0.140.2", + "throttle-debounce": "5.0.0", "tinycolor2": "1.4.2", - "tsc-alias": "1.5.0", - "tsconfig-paths": "3.14.1", + "tsc-alias": "1.6.7", + "tsconfig-paths": "4.0.0", "twemoji-parser": "14.0.0", - "typescript": "4.6.3", "uuid": "8.3.2", "v-debounce": "0.1.2", "vanilla-tilt": "1.7.2", - "vite": "2.9.6", - "vue": "3.2.33", + "vue": "3.2.36", "vue-prism-editor": "2.0.0-alpha.2", - "vue-router": "4.0.14", + "vue-router": "4.0.15", "vuedraggable": "4.0.1", "websocket": "1.0.34", - "ws": "8.5.0" + "@vitejs/plugin-vue": "2.3.3", + "@vue/compiler-sfc": "3.2.36", + "@rollup/plugin-alias": "3.1.9", + "@rollup/plugin-json": "4.1.0", + "rollup": "2.74.1", + "typescript": "4.7.2", + "vite": "2.9.9", + "ws": "8.6.0" }, "devDependencies": { "@types/escape-regexp": "0.0.1", @@ -97,19 +91,21 @@ "@types/matter-js": "0.17.7", "@types/mocha": "9.1.1", "@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": "4.0.0", + "@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.20.0", + "@typescript-eslint/eslint-plugin": "5.26.0", + "@typescript-eslint/parser": "5.26.0", + "eslint": "8.16.0", + "eslint-plugin-vue": "9.0.1", "cross-env": "7.0.3", - "cypress": "9.5.4", + "cypress": "9.7.0", "eslint-plugin-import": "2.26.0", "start-server-and-test": "1.14.0" } diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index 6f806ccc58..ce4af61f18 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -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)); 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 46d45b690f..a947406f88 100644 --- a/packages/client/src/components/abuse-report.vue +++ b/packages/client/src/components/abuse-report.vue @@ -2,7 +2,7 @@
- + diff --git a/packages/client/src/components/channel-follow-button.vue b/packages/client/src/components/channel-follow-button.vue index 7bbf5ae663..dff02beec0 100644 --- a/packages/client/src/components/channel-follow-button.vue +++ b/packages/client/src/components/channel-follow-button.vue @@ -48,8 +48,8 @@ async function onClick() { }); isFollowing.value = true; } - } catch (e) { - console.error(e); + } catch (err) { + console.error(err); } finally { wait.value = false; } diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index a7d5206c71..4e9c4e587a 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -7,8 +7,13 @@
- 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/image-viewer.vue b/packages/client/src/components/image-viewer.vue index c39076df16..7bc88399ef 100644 --- a/packages/client/src/components/image-viewer.vue +++ b/packages/client/src/components/image-viewer.vue @@ -25,7 +25,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (e: 'closed'): void; + (ev: 'closed'): void; }>(); const modal = $ref>(); diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue index 9b0a18ec90..c32409ecf4 100644 --- a/packages/client/src/components/instance-ticker.vue +++ b/packages/client/src/components/instance-ticker.vue @@ -39,6 +39,19 @@ const bg = { border-radius: 4px 0 0 4px; overflow: hidden; color: #fff; + text-shadow: /* .866 ≈ sin(60deg) */ + 1px 0 1px #000, + .866px .5px 1px #000, + .5px .866px 1px #000, + 0 1px 1px #000, + -.5px .866px 1px #000, + -.866px .5px 1px #000, + -1px 0 1px #000, + -.866px -.5px 1px #000, + -.5px -.866px 1px #000, + 0 -1px 1px #000, + .5px -.866px 1px #000, + .866px -.5px 1px #000; > .icon { height: 100%; diff --git a/packages/client/src/components/media-caption.vue b/packages/client/src/components/media-caption.vue index ef546f3f70..feed3854f9 100644 --- a/packages/client/src/components/media-caption.vue +++ b/packages/client/src/components/media-caption.vue @@ -77,7 +77,7 @@ export default defineComponent({ computed: { remainingLength(): number { - if (typeof this.inputValue != "string") return 512; + if (typeof this.inputValue !== "string") return 512; return 512 - length(this.inputValue); } }, @@ -116,17 +116,17 @@ export default defineComponent({ } }, - onKeydown(e) { - if (e.which === 27) { // ESC + onKeydown(evt) { + if (evt.which === 27) { // ESC this.cancel(); } }, - onInputKeydown(e) { - if (e.which === 13) { // Enter - if (e.ctrlKey) { - e.preventDefault(); - e.stopPropagation(); + onInputKeydown(evt) { + if (evt.which === 13) { // Enter + if (evt.ctrlKey) { + evt.preventDefault(); + evt.stopPropagation(); this.ok(); } } diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index 6ac410762d..4556a82d55 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -142,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 = diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index d30284ca5f..14bbbd4f3c 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({ diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 3cd7a819d4..bc8a0dd19d 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({ diff --git a/packages/client/src/components/page/page.image.vue b/packages/client/src/components/page/page.image.vue index 04ce74bd7c..6e38a9f424 100644 --- a/packages/client/src/components/page/page.image.vue +++ b/packages/client/src/components/page/page.image.vue @@ -1,34 +1,22 @@ -