Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2023-09-11 16:50:42 +08:00 committed by GitHub
commit 9b2a8022ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
252 changed files with 6482 additions and 8456 deletions

View File

@ -114,6 +114,7 @@ redis:
# Available methods: # Available methods:
# aid ... Short, Millisecond accuracy # aid ... Short, Millisecond accuracy
# aidx ... Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy # ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility # objectid ... This is left for backward compatibility
@ -121,7 +122,7 @@ redis:
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT! # ID SETTINGS AFTER THAT!
id: 'aid' id: 'aidx'
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Other configuration └───────────────────────────────────── #───┘ Other configuration └─────────────────────────────────────

View File

@ -125,6 +125,7 @@ redis:
# Available methods: # Available methods:
# aid ... Short, Millisecond accuracy # aid ... Short, Millisecond accuracy
# aidx ... Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy # ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility # objectid ... This is left for backward compatibility
@ -132,7 +133,7 @@ redis:
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT! # ID SETTINGS AFTER THAT!
id: 'aid' id: 'aidx'
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Other configuration └───────────────────────────────────── #───┘ Other configuration └─────────────────────────────────────

View File

@ -6,7 +6,7 @@
"features": { "features": {
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}, "ghcr.io/devcontainers-contrib/features/pnpm:2": {},
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "20.5.0" "version": "20.5.1"
} }
}, },
"forwardPorts": [3000], "forwardPorts": [3000],

View File

@ -114,6 +114,7 @@ redis:
# Available methods: # Available methods:
# aid ... Short, Millisecond accuracy # aid ... Short, Millisecond accuracy
# aidx ... Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy # ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility # objectid ... This is left for backward compatibility
@ -121,7 +122,7 @@ redis:
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT! # ID SETTINGS AFTER THAT!
id: 'aid' id: 'aidx'
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Other configuration └───────────────────────────────────── #───┘ Other configuration └─────────────────────────────────────

View File

@ -12,4 +12,4 @@ db:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 56312 port: 56312
id: aid id: aidx

View File

@ -9,7 +9,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.0.0
- run: corepack enable - run: corepack enable

View File

@ -10,7 +10,7 @@ jobs:
check_copyright_year: check_copyright_year:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
- run: | - run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!" echo "Please change copyright year!"

View File

@ -13,7 +13,7 @@ jobs:
if: github.repository == 'laoxong/misskey' if: github.repository == 'laoxong/misskey'
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.10.0 uses: docker/setup-buildx-action@v2.10.0

View File

@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.10.0 uses: docker/setup-buildx-action@v2.10.0

View File

@ -14,7 +14,7 @@ jobs:
env: env:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
- run: | - run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb sudo dpkg -i dockle.deb

View File

@ -11,7 +11,7 @@ jobs:
pnpm_install: pnpm_install:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
@ -38,7 +38,7 @@ jobs:
- sw - sw
- misskey-js - misskey-js
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
@ -64,7 +64,7 @@ jobs:
- backend - backend
- misskey-js - misskey-js
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true

View File

@ -53,7 +53,7 @@ jobs:
# Check out merge commit # Check out merge commit
- name: Fork based /deploy checkout - name: Fork based /deploy checkout
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.0.0
with: with:
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'

View File

@ -1,13 +0,0 @@
name: "Reviewer lottery"
on:
pull_request_target:
types: [opened, ready_for_review, reopened]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
- uses: uesteibar/reviewer-lottery@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,112 +0,0 @@
name: Storybook
on:
push:
branches:
- master
- develop
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
env:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v3.6.0
if: github.event_name != 'pull_request_target'
with:
fetch-depth: 0
submodules: true
- uses: actions/checkout@v3.6.0
if: github.event_name == 'pull_request_target'
with:
fetch-depth: 0
submodules: true
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Checkout actual HEAD
if: github.event_name == 'pull_request_target'
id: rev
run: |
echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT
git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3)
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js 20.x
uses: actions/setup-node@v3.8.1
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build misskey-js
run: pnpm --filter misskey-js build
- name: Build storybook
run: pnpm --filter frontend build-storybook
- name: Publish to Chromatic
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master'
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master'
id: chromatic_push
run: |
DIFF="${{ github.event.before }} HEAD"
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
DIFF="HEAD"
fi
CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))"
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
if pnpm --filter frontend chromatic -d storybook-static $(echo "$CHROMATIC_PARAMETER"); then
echo "success=true" >> $GITHUB_OUTPUT
else
echo "success=false" >> $GITHUB_OUTPUT
fi
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.event_name == 'pull_request_target'
id: chromatic_pull_request
run: |
DIFF="${{ steps.rev.outputs.base }} HEAD"
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
DIFF="HEAD"
fi
CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))"
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
BRANCH="${{ github.event.pull_request.head.user.login }}:${{ github.event.pull_request.head.ref }}"
if [ "$BRANCH" = "misskey-dev:${{ github.event.pull_request.head.ref }}" ]; then
BRANCH="${{ github.event.pull_request.head.ref }}"
fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER")
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic detects changes
uses: actions/github-script@v6.4.0
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
})
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: storybook
path: packages/frontend/storybook-static

View File

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.x] node-version: [20.5.1]
services: services:
postgres: postgres:
@ -29,7 +29,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm

View File

@ -13,10 +13,10 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.x] node-version: [20.5.1]
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
@ -51,7 +51,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [20.x] node-version: [20.5.1]
browser: [chrome] browser: [chrome]
services: services:
@ -68,7 +68,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
with: with:
submodules: true submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150 # https://github.com/cypress-io/cypress-docker-images/issues/150

View File

@ -16,12 +16,12 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.x] node-version: [20.5.1]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.0.0
- run: corepack enable - run: corepack enable

View File

@ -16,10 +16,10 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [20.x] node-version: [20.5.1]
steps: steps:
- uses: actions/checkout@v3.6.0 - uses: actions/checkout@v4.0.0
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm

View File

@ -1 +1 @@
20.5.0 20.5.1

View File

@ -21,21 +21,31 @@
- お知らせのバナー表示やダイアログ表示が可能に - お知らせのバナー表示やダイアログ表示が可能に
- お知らせのアイコンを設定可能に - お知らせのアイコンを設定可能に
- チャンネルをセンシティブ指定できるようになりました - チャンネルをセンシティブ指定できるようになりました
- センシティブチャンネルのNoteのReNoteはデフォルトでHome TLに流れるようになりました
- センシティブチャンネルのノートはユーザープロフィールに表示されません
- 二要素認証のバックアップコードが生成されるようになりました ref. https://github.com/MisskeyIO/misskey/pull/121 - 二要素認証のバックアップコードが生成されるようになりました ref. https://github.com/MisskeyIO/misskey/pull/121
- 二要素認証でパスキーをサポートするようになりました
### Client ### Client
- プロフィールにその人が作ったPlayの一覧出せるように - プロフィールにその人が作ったPlayの一覧出せるように
- メニューのスイッチの動作を改善 - メニューのスイッチの動作を改善
- 絵文字ピッカーの検索の表示件数を100件に増加 - 絵文字ピッカーの検索の表示件数を100件に増加
- 投稿フォームのプレビューの表示状態を記憶するように - 投稿フォームのプレビューの表示状態を記憶するように
- ノート詳細ページ読み込み時のパフォーマンスを改善
- AiScriptからMisskeyサーバーAPIを呼び出す際の制限を撤廃 - AiScriptからMisskeyサーバーAPIを呼び出す際の制限を撤廃
- Playで直接投稿フォームを埋め込めるように(`Ui:C:postForm`)
- 通知をテストできるように
- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように - Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように
- Enhance: 自分が押したリアクションのデザインを改善 - Enhance: 自分が押したリアクションのデザインを改善
- Enhance: ノート検索にローカルのみ検索可能なオプションの追加 - Enhance: ノート検索にローカルのみ検索可能なオプションの追加
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように - Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
- Enhance: Renote自体を通報できるように
- Enhance: データセーバーモードの強化
- Enhance: Renoteを管理者権限で削除可能に
- `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました - `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました
- Playの操作を行うAPI TokenをAPIコンソールから発行できるように - Playの操作を行うAPI TokenをAPIコンソールから発行できるように
- リアクションの表示サイズをより大きくできるように
- ノート詳細ページ読み込み時のパフォーマンスを改善
- タイムラインでリスト/アンテナ選択時のパフォーマンスを改善
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正 - Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正 - Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正
- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正 - Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正
@ -45,10 +55,14 @@
- Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正 - Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正
### Server ### Server
- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように
- cacheRemoteFilesの初期値はfalseになりました - cacheRemoteFilesの初期値はfalseになりました
- ファイルアップロード時等にファイル名の拡張子を修正する関数(correctFilename)の挙動を改善 - ファイルアップロード時等にファイル名の拡張子を修正する関数(correctFilename)の挙動を改善
- Webhookのペイロードにサーバーのurlが含まれるようになりました - Webhookのペイロードにサーバーのurlが含まれるようになりました
- Webhook設定でsecretを空に出来るように
- 使われていないアンテナの自動停止を設定可能に
- nodeinfo 2.1対応
- 自分へのメンション一覧を取得する際のパフォーマンスを向上
- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように
- Fix: 一部のfeatured noteを照会できない問題を修正 - Fix: 一部のfeatured noteを照会できない問題を修正
- Fix: muteがapiからのuser list timeline取得で機能しない問題を修正 - Fix: muteがapiからのuser list timeline取得で機能しない問題を修正
- Fix: ジョブキュー管理画面の認証を回避できる問題を修正 - Fix: ジョブキュー管理画面の認証を回避できる問題を修正

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4 # syntax = docker/dockerfile:1.4
ARG NODE_VERSION=20.5.0-bullseye ARG NODE_VERSION=20.5.1-bullseye
# build assets & compile TypeScript # build assets & compile TypeScript

View File

@ -135,6 +135,7 @@ redis:
# Available methods: # Available methods:
# aid ... Short, Millisecond accuracy # aid ... Short, Millisecond accuracy
# aidx ... Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy # ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility # objectid ... This is left for backward compatibility
@ -142,7 +143,7 @@ redis:
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT! # ID SETTINGS AFTER THAT!
id: "aid" id: "aidx"
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Other configuration └───────────────────────────────────── #───┘ Other configuration └─────────────────────────────────────

View File

@ -1,65 +0,0 @@
/**
* Gulp tasks
*/
import * as fs from 'node:fs';
import gulp from 'gulp';
import replace from 'gulp-replace';
import terser from 'gulp-terser';
import cssnano from 'gulp-cssnano';
import locales from './locales/index.js';
import meta from './package.json' assert { type: "json" };
gulp.task('copy:backend:views', () =>
gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
);
gulp.task('copy:frontend:fonts', () =>
gulp.src('./packages/frontend/node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/_frontend_dist_/fonts/'))
);
gulp.task('copy:frontend:tabler-icons', () =>
gulp.src('./packages/frontend/node_modules/@tabler/icons-webfont/**/*').pipe(gulp.dest('./built/_frontend_dist_/tabler-icons/'))
);
gulp.task('copy:frontend:locales', cb => {
fs.mkdirSync('./built/_frontend_dist_/locales', { recursive: true });
const v = { '_version_': meta.version };
for (const [lang, locale] of Object.entries(locales)) {
fs.writeFileSync(`./built/_frontend_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8');
}
cb();
});
gulp.task('build:backend:script', () => {
return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js'])
.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
.pipe(terser({
toplevel: true
}))
.pipe(gulp.dest('./packages/backend/built/server/web/'));
});
gulp.task('build:backend:style', () => {
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css'])
.pipe(cssnano({
zindex: false
}))
.pipe(gulp.dest('./packages/backend/built/server/web/'));
});
gulp.task('build', gulp.parallel(
'copy:frontend:locales', 'copy:backend:views', 'build:backend:script', 'build:backend:style', 'copy:frontend:fonts', 'copy:frontend:tabler-icons'
));
gulp.task('default', gulp.task('build'));
gulp.task('watch', () => {
gulp.watch([
'./packages/*/src/**/*',
], { ignoreInitial: false }, gulp.task('build'));
});

View File

@ -799,6 +799,7 @@ accountDeletionInProgress: "حذف الحساب جارٍ"
usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله." usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله."
devMode: "وضع المُطوّر" devMode: "وضع المُطوّر"
keepCw: "أبقِ على تحذيرات المحتوى" keepCw: "أبقِ على تحذيرات المحتوى"
pubSub: "حسابات Pub/Sub"
lastCommunication: "آخر تواصل" lastCommunication: "آخر تواصل"
resolved: "عولج" resolved: "عولج"
unresolved: "لم يعالج" unresolved: "لم يعالج"
@ -807,6 +808,7 @@ breakFollowConfirm: "أمتأكد من إزالة المتابِع ؟"
itsOn: "مفعّل" itsOn: "مفعّل"
itsOff: "معطّل" itsOff: "معطّل"
on: "مفعل" on: "مفعل"
off: "معطل"
emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل" emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
unread: "غير مقروءة" unread: "غير مقروءة"
filter: "رشّح" filter: "رشّح"
@ -853,6 +855,7 @@ recentNDays: "آخر {n} أيام"
noEmailServerWarning: "خادم البريد غير مضبوط." noEmailServerWarning: "خادم البريد غير مضبوط."
thereIsUnresolvedAbuseReportWarning: "توجد بلاغات غير معالجة." thereIsUnresolvedAbuseReportWarning: "توجد بلاغات غير معالجة."
recommended: "مقترح" recommended: "مقترح"
check: "التحقق"
driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم" driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم"
driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل." driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل."
requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير." requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير."
@ -876,6 +879,7 @@ slow: "بطيء"
fast: "سريع" fast: "سريع"
sensitiveMediaDetection: "التعرف على المحتوى الحساس" sensitiveMediaDetection: "التعرف على المحتوى الحساس"
localOnly: "المحلي فقط" localOnly: "المحلي فقط"
remoteOnly: "بُعدي فقط"
failedToUpload: "فشل الرفع" failedToUpload: "فشل الرفع"
cannotUploadBecauseInappropriate: "تعذر رفع الملف لوجود محتوى حساس فيه." cannotUploadBecauseInappropriate: "تعذر رفع الملف لوجود محتوى حساس فيه."
cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين." cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين."
@ -895,6 +899,7 @@ pushNotificationAlreadySubscribed: "إرسال الإشعارات مفعل سل
pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها." pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها."
sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها" sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها"
sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك." sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك."
windowMaximize: "املأ الشاشة"
windowRestore: "استرجاع" windowRestore: "استرجاع"
caption: "التعليق التوضيحي" caption: "التعليق التوضيحي"
loggedInAsBot: "والج كآلي" loggedInAsBot: "والج كآلي"
@ -952,6 +957,8 @@ accountMoved: "نقل هذا المستخدم حسابه:"
accountMovedShort: "رُحل هذا الحساب." accountMovedShort: "رُحل هذا الحساب."
operationForbidden: "عملية ممنوعة" operationForbidden: "عملية ممنوعة"
forceShowAds: "أظهر الإعلانات التجارية دائما" forceShowAds: "أظهر الإعلانات التجارية دائما"
reactionsList: "التفاعلات"
renotesList: "إعادات النشر"
leftTop: "أعلى اليسار" leftTop: "أعلى اليسار"
rightTop: "أعلى اليمين" rightTop: "أعلى اليمين"
leftBottom: "أسفل اليسار" leftBottom: "أسفل اليسار"
@ -987,6 +994,9 @@ later: "لاحقاً"
goToMisskey: "لميسكي" goToMisskey: "لميسكي"
additionalEmojiDictionary: "قواميس إيموجي إضافية" additionalEmojiDictionary: "قواميس إيموجي إضافية"
installed: "مُثبت" installed: "مُثبت"
expirationDate: "تاريخ انتهاء الصلاحية"
unused: "غير مستعمَل"
expired: "منتهية صلاحيته"
icon: "الصورة الرمزية" icon: "الصورة الرمزية"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "نجح إنشاء حسابك!" accountCreated: "نجح إنشاء حسابك!"
@ -1073,6 +1083,7 @@ _role:
description: "وصف الدور" description: "وصف الدور"
permission: "أذونات الدور" permission: "أذونات الدور"
assignTarget: "نوع الإسناد" assignTarget: "نوع الإسناد"
condition: "الشرط"
options: "خيارات" options: "خيارات"
policies: "السياسة العامة" policies: "السياسة العامة"
priority: "الأولوية" priority: "الأولوية"
@ -1128,6 +1139,9 @@ _plugin:
install: "ثبّت إضافات" install: "ثبّت إضافات"
installWarn: "رجاءً لا تثبت إضافات غير موثوقة." installWarn: "رجاءً لا تثبت إضافات غير موثوقة."
manage: "إدارة الإضافات" manage: "إدارة الإضافات"
_preferencesBackups:
createdAt: "تم إنشاؤه: {date} {time}"
updatedAt: "آخر تحديث: {date} {time}"
_registry: _registry:
scope: "الحيّز" scope: "الحيّز"
key: "مفتاح" key: "مفتاح"
@ -1252,6 +1266,9 @@ _time:
minute: "د" minute: "د"
hour: "سا" hour: "سا"
day: "ي" day: "ي"
_timelineTutorial:
title: "كيف تستخدم Misskey"
step3_1: "هل نشرت ملاحظتك الأولى؟"
_2fa: _2fa:
alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين." alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين."
step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})." step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})."
@ -1520,6 +1537,8 @@ _deck:
swapUp: "التحريك إلى الأعلى" swapUp: "التحريك إلى الأعلى"
swapDown: "التحريك إلى الأسفل" swapDown: "التحريك إلى الأسفل"
profile: "حسابي الشخصي" profile: "حسابي الشخصي"
newProfile: "ملف تعريفي جديد"
deleteProfile: "حذف الملف التعريفي"
_columns: _columns:
main: "الرئيسية" main: "الرئيسية"
widgets: "التطبيقات المُصغّرة" widgets: "التطبيقات المُصغّرة"

View File

@ -1042,7 +1042,6 @@ _2fa:
alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷" alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷"
step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷" step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷"
step2: "এরপরে, অ্যাপের সাহায্যে প্রদর্শিত QR কোডটি স্ক্যান করুন।" step2: "এরপরে, অ্যাপের সাহায্যে প্রদর্শিত QR কোডটি স্ক্যান করুন।"
step2Url: "ডেস্কটপ অ্যাপে, নিম্নলিখিত URL লিখুন:"
step3: "অ্যাপে প্রদর্শিত টোকেনটি লিখুন এবং আপনার কাজ শেষ।" step3: "অ্যাপে প্রদর্শিত টোকেনটি লিখুন এবং আপনার কাজ শেষ।"
step4: "আপনাকে এখন থেকে লগ ইন করার সময়, এইভাবে টোকেন লিখতে হবে।" step4: "আপনাকে এখন থেকে লগ ইন করার সময়, এইভাবে টোকেন লিখতে হবে।"
securityKeyInfo: "আপনি একটি হার্ডওয়্যার সিকিউরিটি কী ব্যবহার করে লগ ইন করতে পারেন যা FIDO2 বা ডিভাইসের ফিঙ্গারপ্রিন্ট সেন্সর বা পিন সমর্থন করে৷" securityKeyInfo: "আপনি একটি হার্ডওয়্যার সিকিউরিটি কী ব্যবহার করে লগ ইন করতে পারেন যা FIDO2 বা ডিভাইসের ফিঙ্গারপ্রিন্ট সেন্সর বা পিন সমর্থন করে৷"

View File

@ -399,7 +399,6 @@ _sfx:
chat: "Xat" chat: "Xat"
antenna: "Antenes" antenna: "Antenes"
_2fa: _2fa:
step2Url: "També pots inserir aquest enllaç i utilitzes una aplicació d'escriptori:"
renewTOTPCancel: "No, gràcies" renewTOTPCancel: "No, gràcies"
_antennaSources: _antennaSources:
all: "Totes les publicacions" all: "Totes les publicacions"

View File

@ -1681,7 +1681,6 @@ _2fa:
step1: "Nejprve si do zařízení nainstalujte aplikaci pro ověřování (například {a} nebo {b})." step1: "Nejprve si do zařízení nainstalujte aplikaci pro ověřování (například {a} nebo {b})."
step2: "Poté naskenujte QR kód zobrazený na této obrazovce." step2: "Poté naskenujte QR kód zobrazený na této obrazovce."
step2Click: "Kliknutím na tento QR kód můžete zaregistrovat 2FA do bezpečnostního klíče nebo aplikace autentizace telefonu." step2Click: "Kliknutím na tento QR kód můžete zaregistrovat 2FA do bezpečnostního klíče nebo aplikace autentizace telefonu."
step2Url: "Tuto adresu URL můžete zadat také v případě, že používáte program pro stolní počítače:"
step3Title: "Zadejte ověřovací kód" step3Title: "Zadejte ověřovací kód"
step3: "Pro dokončení nastavení zadejte token poskytnutý vaší aplikací." step3: "Pro dokončení nastavení zadejte token poskytnutý vaší aplikací."
step4: "Od této chvíle budou všechny budoucí pokusy o přihlášení vyžadovat tento přihlašovací token." step4: "Od této chvíle budou všechny budoucí pokusy o přihlášení vyžadovat tento přihlašovací token."

View File

@ -45,6 +45,7 @@ pin: "An dein Profil anheften"
unpin: "Von deinem Profil lösen" unpin: "Von deinem Profil lösen"
copyContent: "Inhalt kopieren" copyContent: "Inhalt kopieren"
copyLink: "Link kopieren" copyLink: "Link kopieren"
copyLinkRenote: "Renote-Link kopieren"
delete: "Löschen" delete: "Löschen"
deleteAndEdit: "Löschen und Bearbeiten" deleteAndEdit: "Löschen und Bearbeiten"
deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen." deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen."
@ -411,6 +412,7 @@ aboutMisskey: "Über Misskey"
administrator: "Administrator" administrator: "Administrator"
token: "Token" token: "Token"
2fa: "Zwei-Faktor-Authentifizierung" 2fa: "Zwei-Faktor-Authentifizierung"
setupOf2fa: "Zweifaktorauthentifizierung einrichten"
totp: "Authentifizierungs-App" totp: "Authentifizierungs-App"
totpDescription: "Logge dich via Authentifizierungs-App mit Einmalpasswort ein" totpDescription: "Logge dich via Authentifizierungs-App mit Einmalpasswort ein"
moderator: "Moderator" moderator: "Moderator"
@ -654,6 +656,7 @@ behavior: "Verhalten"
sample: "Beispiel" sample: "Beispiel"
abuseReports: "Meldungen" abuseReports: "Meldungen"
reportAbuse: "Melden" reportAbuse: "Melden"
reportAbuseRenote: "Renote melden"
reportAbuseOf: "{name} melden" reportAbuseOf: "{name} melden"
fillAbuseReportDescription: "Bitte gib zusätzliche Informationen zu dieser Meldung an. Falls es sich um eine spezielle Notiz handelt, bitte gib dessen URL an." fillAbuseReportDescription: "Bitte gib zusätzliche Informationen zu dieser Meldung an. Falls es sich um eine spezielle Notiz handelt, bitte gib dessen URL an."
abuseReported: "Deine Meldung wurde versendet. Vielen Dank." abuseReported: "Deine Meldung wurde versendet. Vielen Dank."
@ -1696,9 +1699,10 @@ _2fa:
step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät."
step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät."
step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren." step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren."
step2Url: "Nutzt du ein Desktopprogramm kannst du alternativ diese URL eingeben:" step2Uri: "Nutzt du ein Desktopprogramm, gib folgende URI eingeben"
step3Title: "Authentifizierungsscode eingeben" step3Title: "Authentifizierungsscode eingeben"
step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird." step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird."
setupCompleted: "Einrichtung abgeschlossen"
step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen." step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen."
securityKeyNotSupported: "Dein Browser unterstützt keine Security-Tokens." securityKeyNotSupported: "Dein Browser unterstützt keine Security-Tokens."
registerTOTPBeforeKey: "Um einen Security-Token oder einen Passkey zu registrieren, musst du zuerst eine Authentifizierungs-App registrieren." registerTOTPBeforeKey: "Um einen Security-Token oder einen Passkey zu registrieren, musst du zuerst eine Authentifizierungs-App registrieren."
@ -1714,6 +1718,11 @@ _2fa:
renewTOTPConfirm: "Codes der bisherigen App werden hierdurch nutzlos" renewTOTPConfirm: "Codes der bisherigen App werden hierdurch nutzlos"
renewTOTPOk: "Neu einrichten" renewTOTPOk: "Neu einrichten"
renewTOTPCancel: "Abbrechen" renewTOTPCancel: "Abbrechen"
checkBackupCodesBeforeCloseThisWizard: "Notiere bitte deine Backup-Codes, bevor du dieses Fenster schließt."
backupCodes: "Backup-Codes"
backupCodesDescription: "Verwende diese Codes, falls du nicht mehr auf deine App zur Zweifaktorauthentifizierung zugreifen kannst. Jeder Code kann nur einmal verwendet werden. Bewahre sie an einem sicheren Ort auf."
backupCodeUsedWarning: "Ein Backup-Code wurde verwendet. Falls du den Zugriff zu deiner Zweifaktorauthentifizierungsapp verloren hast, konfiguriere diese bitte möglichst bald erneut."
backupCodesExhaustedWarning: "Alle Backup-Codes wurden verwendet. Falls du den Zugang zu deiner Zweifaktorauthentifizierungsapp verlierst, wirst du dich nicht mehr in dieses Konto einloggen können. Bitte konfiguriere diese App erneut."
_permissions: _permissions:
"read:account": "Deine Benutzerkontoinformationen lesen" "read:account": "Deine Benutzerkontoinformationen lesen"
"write:account": "Deine Benutzerkontoinformationen bearbeiten" "write:account": "Deine Benutzerkontoinformationen bearbeiten"
@ -2021,6 +2030,8 @@ _deck:
introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen." introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen."
widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu." widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu."
useSimpleUiForNonRootPages: "Simple Benutzeroberfläche für navigierte Seiten verwenden" useSimpleUiForNonRootPages: "Simple Benutzeroberfläche für navigierte Seiten verwenden"
usedAsMinWidthWhenFlexible: "Ist \"Automatische Breitenanpassung\" aktiviert, wird hierfür die minimale Breite verwendet"
flexible: "Automatische Breitenanpassung"
_columns: _columns:
main: "Hauptspalte" main: "Hauptspalte"
widgets: "Widgets" widgets: "Widgets"

View File

@ -45,6 +45,7 @@ pin: "Pin to profile"
unpin: "Unpin from profile" unpin: "Unpin from profile"
copyContent: "Copy contents" copyContent: "Copy contents"
copyLink: "Copy link" copyLink: "Copy link"
copyLinkRenote: "Copy renote link"
delete: "Delete" delete: "Delete"
deleteAndEdit: "Delete and edit" deleteAndEdit: "Delete and edit"
deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it." deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it."
@ -411,6 +412,7 @@ aboutMisskey: "About Misskey"
administrator: "Administrator" administrator: "Administrator"
token: "Token" token: "Token"
2fa: "Two-factor authentication" 2fa: "Two-factor authentication"
setupOf2fa: "Setup two-factor authentification"
totp: "Authenticator App" totp: "Authenticator App"
totpDescription: "Use an authenticator app to enter one-time passwords" totpDescription: "Use an authenticator app to enter one-time passwords"
moderator: "Moderator" moderator: "Moderator"
@ -654,6 +656,7 @@ behavior: "Behavior"
sample: "Sample" sample: "Sample"
abuseReports: "Reports" abuseReports: "Reports"
reportAbuse: "Report" reportAbuse: "Report"
reportAbuseRenote: "Report renote"
reportAbuseOf: "Report {name}" reportAbuseOf: "Report {name}"
fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific note, please include its URL." fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific note, please include its URL."
abuseReported: "Your report has been sent. Thank you very much." abuseReported: "Your report has been sent. Thank you very much."
@ -1696,9 +1699,10 @@ _2fa:
step1: "First, install an authentication app (such as {a} or {b}) on your device." step1: "First, install an authentication app (such as {a} or {b}) on your device."
step2: "Then, scan the QR code displayed on this screen." step2: "Then, scan the QR code displayed on this screen."
step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app." step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app."
step2Url: "You can also enter this URL if you're using a desktop program:" step2Uri: "Enter the following URI if you are using a desktop program"
step3Title: "Enter an authentication code" step3Title: "Enter an authentication code"
step3: "Enter the token provided by your app to finish setup." step3: "Enter the token provided by your app to finish setup."
setupCompleted: "Setup complete"
step4: "From now on, any future login attempts will ask for such a login token." step4: "From now on, any future login attempts will ask for such a login token."
securityKeyNotSupported: "Your browser does not support security keys." securityKeyNotSupported: "Your browser does not support security keys."
registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key." registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key."
@ -1714,6 +1718,11 @@ _2fa:
renewTOTPConfirm: "This will cause verification codes from your previous app to stop working" renewTOTPConfirm: "This will cause verification codes from your previous app to stop working"
renewTOTPOk: "Reconfigure" renewTOTPOk: "Reconfigure"
renewTOTPCancel: "Cancel" renewTOTPCancel: "Cancel"
checkBackupCodesBeforeCloseThisWizard: "Before you close this window, please note the following backup codes."
backupCodes: "Backup codes"
backupCodesDescription: "You can use these codes to gain access to your account in case of becoming unable to use your two-factor authentificator app. Each can only be used once. Please keep them in a safe place."
backupCodeUsedWarning: "A backup code has been used. Please reconfigure two-factor authentification as soon as possible if you are no longer able to use it."
backupCodesExhaustedWarning: "All backup codes have been used. Should you lose access to your two-factor authentification app, you will be unable to access this account. Please reconfigure two-factor authentification."
_permissions: _permissions:
"read:account": "View your account information" "read:account": "View your account information"
"write:account": "Edit your account information" "write:account": "Edit your account information"
@ -2021,6 +2030,8 @@ _deck:
introduction2: "Click on the + on the right of the screen to add new colums whenever you want." introduction2: "Click on the + on the right of the screen to add new colums whenever you want."
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget." widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."
useSimpleUiForNonRootPages: "Use simplified UI to navigated pages" useSimpleUiForNonRootPages: "Use simplified UI to navigated pages"
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
flexible: "Auto-adjust width"
_columns: _columns:
main: "Main" main: "Main"
widgets: "Widgets" widgets: "Widgets"

View File

@ -45,6 +45,7 @@ pin: "Fijar al perfil"
unpin: "Desfijar" unpin: "Desfijar"
copyContent: "Copiar contenido" copyContent: "Copiar contenido"
copyLink: "Copiar enlace" copyLink: "Copiar enlace"
copyLinkRenote: "Copiar enlace de renota"
delete: "Borrar" delete: "Borrar"
deleteAndEdit: "Borrar y editar" deleteAndEdit: "Borrar y editar"
deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas." deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas."
@ -411,6 +412,7 @@ aboutMisskey: "Sobre Misskey"
administrator: "Administrador" administrator: "Administrador"
token: "Token" token: "Token"
2fa: "Autenticación de doble factor" 2fa: "Autenticación de doble factor"
setupOf2fa: "Configurar la autenticación de dos factores"
totp: "Aplicación autentícadora" totp: "Aplicación autentícadora"
totpDescription: "Ingresa una contaseña de un sólo uso usando la aplicación autenticadora" totpDescription: "Ingresa una contaseña de un sólo uso usando la aplicación autenticadora"
moderator: "Moderador" moderator: "Moderador"
@ -654,6 +656,7 @@ behavior: "Comportamiento"
sample: "Muestra" sample: "Muestra"
abuseReports: "Reportes" abuseReports: "Reportes"
reportAbuse: "Reportar" reportAbuse: "Reportar"
reportAbuseRenote: "Reportar renota"
reportAbuseOf: "Reportar a {name}" reportAbuseOf: "Reportar a {name}"
fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en particular, ingrese la URL de esta." 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." abuseReported: "Se ha enviado el reporte. Muchas gracias."
@ -1696,9 +1699,10 @@ _2fa:
step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra." step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra."
step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla." step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla."
step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app.\nTocar este código QR te permitirá registrar la autenticación 2FA a tu llave de seguridad o aplicación autenticadora." step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app.\nTocar este código QR te permitirá registrar la autenticación 2FA a tu llave de seguridad o aplicación autenticadora."
step2Url: "En una aplicación de escritorio se puede ingresar la siguiente URL:" step2Uri: "Si usas una aplicación de escritorio, introduce en ella la siguiente URL."
step3Title: "Ingresa un código de autenticación" step3Title: "Ingresa un código de autenticación"
step3: "Para terminar, ingrese el token mostrado en la aplicación." step3: "Para terminar, ingrese el token mostrado en la aplicación."
setupCompleted: "Configuración completada"
step4: "Ahora cuando inicie sesión, ingrese el mismo token" step4: "Ahora cuando inicie sesión, ingrese el mismo token"
securityKeyNotSupported: "Tu navegador no soporta claves de autenticación." securityKeyNotSupported: "Tu navegador no soporta claves de autenticación."
registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key.\npor favor. configura una aplicación de autenticación para registrar una llave de seguridad." registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key.\npor favor. configura una aplicación de autenticación para registrar una llave de seguridad."
@ -1714,6 +1718,11 @@ _2fa:
renewTOTPConfirm: "This will cause verification codes from your previous app to stop working\nEsto hará que los códigos de verificación de la aplicación anterior dejen de funcionar" renewTOTPConfirm: "This will cause verification codes from your previous app to stop working\nEsto hará que los códigos de verificación de la aplicación anterior dejen de funcionar"
renewTOTPOk: "Reconfigurar" renewTOTPOk: "Reconfigurar"
renewTOTPCancel: "No gracias" renewTOTPCancel: "No gracias"
checkBackupCodesBeforeCloseThisWizard: "Por favor, copia los siguientes códigos de respaldo antes de finalizar el asistente."
backupCodes: "Códigos de Respaldo"
backupCodesDescription: "En caso de que no puedas usar tu aplicación de autenticación, podrás usar los códigos de respaldo que figuran abajo para acceder a tu cuenta. Asegúrate de guardar en lugar seguro los códigos de respaldo. Cada uno de los códigos de respaldo es de un solo uso."
backupCodeUsedWarning: "Has usado todos los códigos de respaldo. Si dejas de tener acceso a tu aplicación de autenticación, no podrás volver a iniciar sesión en tu cuenta. Por favor, reconfigura tu aplicación de autenticación lo antes posible."
backupCodesExhaustedWarning: "Has usado todos los códigos de respaldo. Si dejas de tener acceso a tu aplicación de autenticación, no podrás volver a iniciar sesión en la cuenta que figura arriba. Por favor, reconfigura tu aplicación de autenticación lo antes posible."
_permissions: _permissions:
"read:account": "Ver información de la cuenta" "read:account": "Ver información de la cuenta"
"write:account": "Editar información de la cuenta" "write:account": "Editar información de la cuenta"
@ -1747,6 +1756,10 @@ _permissions:
"write:gallery": "Editar galería" "write:gallery": "Editar galería"
"read:gallery-likes": "Ver favoritos de la galería" "read:gallery-likes": "Ver favoritos de la galería"
"write:gallery-likes": "Editar favoritos de la galería" "write:gallery-likes": "Editar favoritos de la galería"
"read:flash": "Ver Play"
"write:flash": "Editar Plays"
"read:flash-likes": "Ver los Play que me gustan"
"write:flash-likes": "Editar lista de Play que me gustan"
_auth: _auth:
shareAccessTitle: "Permisos de la aplicación" shareAccessTitle: "Permisos de la aplicación"
shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?" shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?"
@ -2017,6 +2030,8 @@ _deck:
introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras." introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras."
widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget." widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget."
useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple" useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple"
usedAsMinWidthWhenFlexible: "Se usará el ancho mínimo cuando la opción \"Autoajustar ancho\" esté habilitada"
flexible: "Autoajustar ancho"
_columns: _columns:
main: "Principal" main: "Principal"
widgets: "Widgets" widgets: "Widgets"

View File

@ -251,7 +251,7 @@ announcements: "Annonces"
imageUrl: "URL de limage" imageUrl: "URL de limage"
remove: "Supprimer" remove: "Supprimer"
removed: "Supprimé" removed: "Supprimé"
removeAreYouSure: "Êtes-vous sûr·e de vouloir supprimer「{x}」?" removeAreYouSure: "Êtes-vous sûr·e de vouloir supprimer « {x} » ?"
deleteAreYouSure: "Êtes-vous sûr·e de vouloir supprimer「{x}」?" deleteAreYouSure: "Êtes-vous sûr·e de vouloir supprimer「{x}」?"
resetAreYouSure: "Voulez-vous réinitialiser ?" resetAreYouSure: "Voulez-vous réinitialiser ?"
saved: "Enregistré" saved: "Enregistré"
@ -600,7 +600,7 @@ tokenRequested: "Autoriser l'accès au compte"
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
notificationType: "Type de notifications" notificationType: "Type de notifications"
edit: "Editer" edit: "Editer"
emailServer: "Serveur mail" emailServer: "Serveur de messagerie"
enableEmail: "Activer la distribution de courriel" enableEmail: "Activer la distribution de courriel"
emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli." emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli."
email: "E-mail " email: "E-mail "
@ -835,7 +835,7 @@ off: "Désactivé"
emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte" emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte"
unread: "Non lu" unread: "Non lu"
filter: "Filtre" filter: "Filtre"
controlPanel: "Panneau de contrôle" controlPanel: "Panneau de configuration"
manageAccounts: "Gérer les comptes" manageAccounts: "Gérer les comptes"
makeReactionsPublic: "Rendre les réactions publiques" makeReactionsPublic: "Rendre les réactions publiques"
makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique." makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique."
@ -945,9 +945,12 @@ color: "Couleur"
manageCustomEmojis: "Gestion des émojis personnalisés" manageCustomEmojis: "Gestion des émojis personnalisés"
preset: "Préréglage" preset: "Préréglage"
selectFromPresets: "Sélectionner à partir des préréglages" selectFromPresets: "Sélectionner à partir des préréglages"
achievements: "Accomplissements"
thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes." thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes."
thisPostMayBeAnnoyingCancel: "Annuler" thisPostMayBeAnnoyingCancel: "Annuler"
thisPostMayBeAnnoyingIgnore: "Publier quand-même"
internalServerError: "Erreur interne du serveur" internalServerError: "Erreur interne du serveur"
disableFederationOk: "Désactiver"
license: "Licence" license: "Licence"
video: "Vidéo" video: "Vidéo"
videos: "Vidéos" videos: "Vidéos"
@ -964,14 +967,33 @@ vertical: "Vertical"
horizontal: "Latéral" horizontal: "Latéral"
serverRules: "Règles du serveur" serverRules: "Règles du serveur"
youFollowing: "Abonné·e" youFollowing: "Abonné·e"
later: "Plus tard"
goToMisskey: "Retour vers Misskey" goToMisskey: "Retour vers Misskey"
expirationDate: "Date dexpiration" expirationDate: "Date dexpiration"
usedAt: "Utilisé le"
unused: "Non-utilisé"
used: "Utilisé"
expired: "Expiré"
doYouAgree: "Êtes-vous daccord ?"
icon: "Avatar" icon: "Avatar"
forYou: "Pour vous"
_announcement:
readConfirmTitle: "Marquer comme lu ?"
_initialAccountSetting:
profileSetting: "Paramètres du profil"
privacySetting: "Paramètres de confidentialité"
_accountMigration:
moveToLabel: "Compte vers lequel vous migrez :"
startMigration: "Migrer"
movedTo: "Compte vers lequel vous migrez :"
_achievements: _achievements:
_types: _types:
_notes1: _notes1:
title: "Je viens tout juste de configurer mon msky"
description: "Publiez votre première note" description: "Publiez votre première note"
flavor: "Passez un bon moment avec Misskey !" flavor: "Passez un bon moment avec Misskey !"
_notes10:
title: "Quelques notes"
_notes100: _notes100:
title: "Beaucoup de notes" title: "Beaucoup de notes"
_notes100000: _notes100000:
@ -986,16 +1008,23 @@ _achievements:
title: "Débutant Ⅲ" title: "Débutant Ⅲ"
description: "Se connecter pour un total de 15 jours" description: "Se connecter pour un total de 15 jours"
_login30: _login30:
title: "Misskeynaute I"
description: "Se connecter pour un total de 30 jours" description: "Se connecter pour un total de 30 jours"
_login60: _login60:
title: "Misskeynaute II"
description: "Se connecter pour un total de 60 jours" description: "Se connecter pour un total de 60 jours"
_login100: _login100:
title: "Misskeynaute III"
description: "Se connecter pour un total de 100 jours" description: "Se connecter pour un total de 100 jours"
flavor: "Misskeynaute acharné·e"
_login200: _login200:
title: "Régulier I"
description: "Se connecter pour un total de 200 jours" description: "Se connecter pour un total de 200 jours"
_login300: _login300:
title: "Régulier II"
description: "Se connecter pour un total de 300 jours" description: "Se connecter pour un total de 300 jours"
_login400: _login400:
title: "Régulier III"
description: "Se connecter pour un total de 400 jours" description: "Se connecter pour un total de 400 jours"
_login500: _login500:
description: "Se connecter pour un total de 500 jours" description: "Se connecter pour un total de 500 jours"
@ -1009,6 +1038,8 @@ _achievements:
description: "Se connecter pour un total de 900 jours" description: "Se connecter pour un total de 900 jours"
_login1000: _login1000:
flavor: "Merci d'utiliser Misskey !" flavor: "Merci d'utiliser Misskey !"
_profileFilled:
description: "Configuration de votre profil"
_markedAsCat: _markedAsCat:
title: "Je suis un chat" title: "Je suis un chat"
flavor: "Je n'ai pas encore de nom" flavor: "Je n'ai pas encore de nom"
@ -1018,6 +1049,16 @@ _achievements:
title: "Abonnez-moi !" title: "Abonnez-moi !"
_iLoveMisskey: _iLoveMisskey:
title: "Jadore Misskey" title: "Jadore Misskey"
description: "Publication « J❤ #Misskey »"
_foundTreasure:
title: "Chasse au trésor"
description: "Vous avez trouvé le trésor caché"
_postedAtLateNight:
flavor: "Cest lheure daller au lit."
_postedAt0min0sec:
title: "Horloge parlante"
description: "Publication dune note à 00:00"
flavor: "Tic tac, tic tac, tic tac, ding !"
_viewInstanceChart: _viewInstanceChart:
title: "Analyste" title: "Analyste"
_loggedInOnBirthday: _loggedInOnBirthday:
@ -1027,7 +1068,11 @@ _achievements:
_cookieClicked: _cookieClicked:
flavor: "Attendez une minute, vous êtes sur le mauvais site web ?" flavor: "Attendez une minute, vous êtes sur le mauvais site web ?"
_role: _role:
name: "Nom du rôle"
description: "Description du rôle"
permission: "Rôle et autorisations"
assignTarget: "Attribuer" assignTarget: "Attribuer"
condition: "Condition"
priority: "Priorité" priority: "Priorité"
_priority: _priority:
low: "Basse" low: "Basse"
@ -1119,6 +1164,8 @@ _aboutMisskey:
donate: "Soutenir Misskey" donate: "Soutenir Misskey"
morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰" morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰"
patrons: "Contributeurs" patrons: "Contributeurs"
_displayOfSensitiveMedia:
force: "Masquer tous les médias"
_instanceTicker: _instanceTicker:
none: "Cacher " none: "Cacher "
remote: "Montrer pour les utilisateur·ice·s distant·e·s" remote: "Montrer pour les utilisateur·ice·s distant·e·s"
@ -1137,6 +1184,8 @@ _channel:
following: "Abonné·e" following: "Abonné·e"
usersCount: "{n} Participant·e·s" usersCount: "{n} Participant·e·s"
notesCount: "{n} Notes" notesCount: "{n} Notes"
nameAndDescription: "Nom et description"
nameOnly: "Nom seulement"
_menuDisplay: _menuDisplay:
sideFull: "Latéral" sideFull: "Latéral"
sideIcon: "Latéral (icônes)" sideIcon: "Latéral (icônes)"
@ -1254,16 +1303,24 @@ _time:
minute: "min" minute: "min"
hour: "h" hour: "h"
day: "j" day: "j"
_timelineTutorial:
title: "Comment utiliser Misskey"
step3_1: "Avez-vous publié votre première note ?"
_2fa: _2fa:
alreadyRegistered: "Configuration déjà achevée." alreadyRegistered: "Configuration déjà achevée."
step1: "Tout d'abord, installez une application d'authentification, telle que {a} ou {b}, sur votre appareil." step1: "Tout d'abord, installez une application d'authentification, telle que {a} ou {b}, sur votre appareil."
step2: "Ensuite, scannez le code QR affiché sur lécran." step2: "Ensuite, scannez le code QR affiché sur lécran."
step2Url: "Vous pouvez également saisir cette URL si vous utilisez un programme de bureau :" step3Title: "Veuillez saisir le code dauthentification"
step3: "Entrez le jeton affiché sur votre application pour compléter la configuration." step3: "Entrez le jeton affiché sur votre application pour compléter la configuration."
setupCompleted: "Configuration terminée avec succès !"
step4: "À partir de maintenant, ce même jeton vous sera demandé à chacune de vos connexions." step4: "À partir de maintenant, ce même jeton vous sera demandé à chacune de vos connexions."
securityKeyNotSupported: "Votre navigateur ne prend pas en charge les clés de sécurité."
securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil." securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil."
securityKeyName: "Nom de la clé"
removeKeyConfirm: "Voulez-vous supprimer {name} ?" removeKeyConfirm: "Voulez-vous supprimer {name} ?"
renewTOTPOk: "Reconfigurer"
renewTOTPCancel: "Pas maintenant" renewTOTPCancel: "Pas maintenant"
backupCodes: "Codes de Secours"
_permissions: _permissions:
"read:account": "Afficher les informations du compte" "read:account": "Afficher les informations du compte"
"write:account": "Mettre à jour les informations de votre compte" "write:account": "Mettre à jour les informations de votre compte"
@ -1480,7 +1537,7 @@ _pages:
fontSerif: "Serif" fontSerif: "Serif"
fontSansSerif: "Sans Serif" fontSansSerif: "Sans Serif"
eyeCatchingImageSet: "Définir une image attractive" eyeCatchingImageSet: "Définir une image attractive"
eyeCatchingImageRemove: "Supprimer l'image attractive" eyeCatchingImageRemove: "Supprimer la miniature"
chooseBlock: "Ajouter un bloc" chooseBlock: "Ajouter un bloc"
selectType: "Choisir un type" selectType: "Choisir un type"
contentBlocks: "Contenu" contentBlocks: "Contenu"
@ -1513,6 +1570,7 @@ _notification:
pollEnded: "Les résultats du sondage sont disponibles" pollEnded: "Les résultats du sondage sont disponibles"
unreadAntennaNote: "Antenne {name}" unreadAntennaNote: "Antenne {name}"
emptyPushNotificationMessage: "Les notifications push ont été mises à jour" emptyPushNotificationMessage: "Les notifications push ont été mises à jour"
achievementEarned: "Accomplissement"
_types: _types:
all: "Toutes" all: "Toutes"
follow: "Nouvel·le abonné·e" follow: "Nouvel·le abonné·e"
@ -1524,6 +1582,7 @@ _notification:
pollEnded: "Sondages se cloturant" pollEnded: "Sondages se cloturant"
receiveFollowRequest: "Demande d'abonnement reçue" receiveFollowRequest: "Demande d'abonnement reçue"
followRequestAccepted: "Demande d'abonnement acceptée" followRequestAccepted: "Demande d'abonnement acceptée"
achievementEarned: "Accomplissement"
app: "Notifications provenant des apps" app: "Notifications provenant des apps"
_actions: _actions:
followBack: "Suivre" followBack: "Suivre"
@ -1545,6 +1604,7 @@ _deck:
deleteProfile: "Supprimer le profil" deleteProfile: "Supprimer le profil"
introduction: "Créez linterface parfaite qui vous sied en arrangeant librement les colonnes !" introduction: "Créez linterface parfaite qui vous sied en arrangeant librement les colonnes !"
introduction2: "Cliquez sur le + à droite de l'écran pour ajouter de nouvelles colonnes quand vous le souhaitez." introduction2: "Cliquez sur le + à droite de l'écran pour ajouter de nouvelles colonnes quand vous le souhaitez."
flexible: "Ajuster automatiquement la largeur"
_columns: _columns:
main: "Principale" main: "Principale"
widgets: "Widgets" widgets: "Widgets"

View File

@ -1,6 +1,11 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import * as ts from 'typescript'; import ts from 'typescript';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
function createMembers(record) { function createMembers(record) {
return Object.entries(record) return Object.entries(record)

View File

@ -1095,7 +1095,11 @@ expired: "Kedaluwarsa"
doYouAgree: "Apa kamu setuju?" doYouAgree: "Apa kamu setuju?"
beSureToReadThisAsItIsImportant: "Mohon baca informasi penting berikut." beSureToReadThisAsItIsImportant: "Mohon baca informasi penting berikut."
iHaveReadXCarefullyAndAgree: "Saya telah membaca \"{x}\" dan menyetujui." iHaveReadXCarefullyAndAgree: "Saya telah membaca \"{x}\" dan menyetujui."
dialog: "Dialog"
icon: "Avatar" icon: "Avatar"
forYou: "Untuk Anda"
currentAnnouncements: "Pengumuman Saat Ini"
pastAnnouncements: "Pengumuman Terdahulu"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Akun kamu telah sukses dibuat!" accountCreated: "Akun kamu telah sukses dibuat!"
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu." letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
@ -1681,7 +1685,6 @@ _2fa:
step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu." step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu."
step2: "Lalu, pindai kode QR yang ada di layar." step2: "Lalu, pindai kode QR yang ada di layar."
step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel." step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel."
step2Url: "Di aplikasi desktop, masukkan URL berikut:"
step3Title: "Masukkan kode autentikasi" step3Title: "Masukkan kode autentikasi"
step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan."
step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu." step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu."
@ -1699,6 +1702,7 @@ _2fa:
renewTOTPConfirm: "Hal ini akan menyebabkan kode verifikasi dari aplikasi autentikator sebelumnya berhenti bekerja" renewTOTPConfirm: "Hal ini akan menyebabkan kode verifikasi dari aplikasi autentikator sebelumnya berhenti bekerja"
renewTOTPOk: "Atur ulang" renewTOTPOk: "Atur ulang"
renewTOTPCancel: "Tidak sekarang." renewTOTPCancel: "Tidak sekarang."
backupCodes: "Kode Pencadangan"
_permissions: _permissions:
"read:account": "Lihat informasi akun" "read:account": "Lihat informasi akun"
"write:account": "Sunting informasi akun" "write:account": "Sunting informasi akun"

12
locales/index.d.ts vendored
View File

@ -48,6 +48,7 @@ export interface Locale {
"unpin": string; "unpin": string;
"copyContent": string; "copyContent": string;
"copyLink": string; "copyLink": string;
"copyLinkRenote": string;
"delete": string; "delete": string;
"deleteAndEdit": string; "deleteAndEdit": string;
"deleteAndEditConfirm": string; "deleteAndEditConfirm": string;
@ -658,6 +659,7 @@ export interface Locale {
"sample": string; "sample": string;
"abuseReports": string; "abuseReports": string;
"reportAbuse": string; "reportAbuse": string;
"reportAbuseRenote": string;
"reportAbuseOf": string; "reportAbuseOf": string;
"fillAbuseReportDescription": string; "fillAbuseReportDescription": string;
"abuseReported": string; "abuseReported": string;
@ -1024,7 +1026,7 @@ export interface Locale {
"enableChartsForRemoteUser": string; "enableChartsForRemoteUser": string;
"enableChartsForFederatedInstances": string; "enableChartsForFederatedInstances": string;
"showClipButtonInNoteFooter": string; "showClipButtonInNoteFooter": string;
"largeNoteReactions": string; "reactionsDisplaySize": string;
"noteIdOrUrl": string; "noteIdOrUrl": string;
"video": string; "video": string;
"videos": string; "videos": string;
@ -1106,6 +1108,7 @@ export interface Locale {
"currentAnnouncements": string; "currentAnnouncements": string;
"pastAnnouncements": string; "pastAnnouncements": string;
"youHaveUnreadAnnouncements": string; "youHaveUnreadAnnouncements": string;
"useSecurityKey": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;
@ -1820,7 +1823,6 @@ export interface Locale {
"securityKeyNotSupported": string; "securityKeyNotSupported": string;
"registerTOTPBeforeKey": string; "registerTOTPBeforeKey": string;
"securityKeyInfo": string; "securityKeyInfo": string;
"chromePasskeyNotSupported": string;
"registerSecurityKey": string; "registerSecurityKey": string;
"securityKeyName": string; "securityKeyName": string;
"tapSecurityKey": string; "tapSecurityKey": string;
@ -2130,6 +2132,10 @@ export interface Locale {
"unreadAntennaNote": string; "unreadAntennaNote": string;
"emptyPushNotificationMessage": string; "emptyPushNotificationMessage": string;
"achievementEarned": string; "achievementEarned": string;
"testNotification": string;
"checkNotificationBehavior": string;
"sendTestNotification": string;
"notificationWillBeDisplayedLikeThis": string;
"_types": { "_types": {
"all": string; "all": string;
"follow": string; "follow": string;
@ -2168,6 +2174,8 @@ export interface Locale {
"introduction2": string; "introduction2": string;
"widgetsIntroduction": string; "widgetsIntroduction": string;
"useSimpleUiForNonRootPages": string; "useSimpleUiForNonRootPages": string;
"usedAsMinWidthWhenFlexible": string;
"flexible": string;
"_columns": { "_columns": {
"main": string; "main": string;
"widgets": string; "widgets": string;

View File

@ -45,6 +45,7 @@ pin: "Fissa sul profilo"
unpin: "Non fissare sul profilo" unpin: "Non fissare sul profilo"
copyContent: "Copia il contenuto" copyContent: "Copia il contenuto"
copyLink: "Copia il link" copyLink: "Copia il link"
copyLinkRenote: "Copia collegamento alla Rinota"
delete: "Elimina" delete: "Elimina"
deleteAndEdit: "Elimina e modifica" deleteAndEdit: "Elimina e modifica"
deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate." deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate."
@ -63,7 +64,7 @@ reply: "Rispondi"
loadMore: "Mostra di più" loadMore: "Mostra di più"
showMore: "Espandi" showMore: "Espandi"
showLess: "Comprimi" showLess: "Comprimi"
youGotNewFollower: "Ha iniziato a seguirti" youGotNewFollower: "Ti sta seguendo"
receiveFollowRequest: "Hai ricevuto una richiesta di follow" receiveFollowRequest: "Hai ricevuto una richiesta di follow"
followRequestAccepted: "Ha accettato la tua richiesta di follow" followRequestAccepted: "Ha accettato la tua richiesta di follow"
mention: "Menzioni" mention: "Menzioni"
@ -74,8 +75,8 @@ import: "Importa"
export: "Esporta" export: "Esporta"
files: "Allegati" files: "Allegati"
download: "Scarica" download: "Scarica"
driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\"? Anche gli allegati verranno eliminati." driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?"
unfollowConfirm: "Vuoi smettere di seguire {name}?" unfollowConfirm: "Vuoi davvero smettere di seguire {name}?"
exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive." exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive."
importRequested: "Hai richiesto un'importazione. Può volerci tempo. " importRequested: "Hai richiesto un'importazione. Può volerci tempo. "
lists: "Liste" lists: "Liste"
@ -84,7 +85,7 @@ note: "Nota"
notes: "Note" notes: "Note"
following: "Follow" following: "Follow"
followers: "Follower" followers: "Follower"
followsYou: "Ti segue" followsYou: "Segue"
createList: "Aggiungi una nuova lista" createList: "Aggiungi una nuova lista"
manageLists: "Gestisci liste" manageLists: "Gestisci liste"
error: "Errore" error: "Errore"
@ -137,7 +138,7 @@ suspend: "Sospendi"
unsuspend: "Revoca la sospensione" unsuspend: "Revoca la sospensione"
blockConfirm: "Vuoi davvero bloccare il profilo?" blockConfirm: "Vuoi davvero bloccare il profilo?"
unblockConfirm: "Vuoi davvero sbloccare il profilo?" unblockConfirm: "Vuoi davvero sbloccare il profilo?"
suspendConfirm: "Vuoi sospendere questo profilo?" suspendConfirm: "Vuoi davvero sospendere questo profilo?"
unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?" unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?"
selectList: "Seleziona una lista" selectList: "Seleziona una lista"
editList: "Modifica Lista" editList: "Modifica Lista"
@ -165,7 +166,7 @@ flagAsCat: "Sono un gatto"
flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo" flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo"
flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline." flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline."
flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note" flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note"
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui" autoAcceptFollowed: "Accetta automaticamente le richieste di follow da profili che già segui"
addAccount: "Aggiungi profilo" addAccount: "Aggiungi profilo"
reloadAccountsList: "Ricarica l'elenco dei profili" reloadAccountsList: "Ricarica l'elenco dei profili"
loginFailed: "Accesso non riuscito" loginFailed: "Accesso non riuscito"
@ -255,7 +256,7 @@ imageUrl: "URL dell'immagine"
remove: "Elimina" remove: "Elimina"
removed: "Eliminato con successo" removed: "Eliminato con successo"
removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?" removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
deleteAreYouSure: "Eliminare \"{x}\"?" deleteAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
resetAreYouSure: "Ripristinare?" resetAreYouSure: "Ripristinare?"
saved: "Salvato" saved: "Salvato"
messaging: "Messaggi" messaging: "Messaggi"
@ -395,9 +396,9 @@ connectedTo: "Connessione ai seguenti profili:"
notesAndReplies: "Note e risposte" notesAndReplies: "Note e risposte"
withFiles: "Con file in allegato" withFiles: "Con file in allegato"
silence: "Silenzia" silence: "Silenzia"
silenceConfirm: "Vuoi davvero silenziare l'utente?" silenceConfirm: "Vuoi davvero silenziare questo profilo?"
unsilence: "Riattiva" unsilence: "Riattiva"
unsilenceConfirm: "Vuoi davvero riattivare l'utente?" unsilenceConfirm: "Vuoi davvero riattivare questo profilo?"
popularUsers: "Utenti popolari" popularUsers: "Utenti popolari"
recentlyUpdatedUsers: "Utenti attivi di recente" recentlyUpdatedUsers: "Utenti attivi di recente"
recentlyRegisteredUsers: "Utenti registrati di recente" recentlyRegisteredUsers: "Utenti registrati di recente"
@ -411,6 +412,7 @@ aboutMisskey: "Informazioni di Misskey"
administrator: "Amministratore" administrator: "Amministratore"
token: "Token" token: "Token"
2fa: "Autenticazione a due fattori" 2fa: "Autenticazione a due fattori"
setupOf2fa: "Impostare l'autenticazione a due fattori"
totp: "App di autenticazione" totp: "App di autenticazione"
totpDescription: "Inserisci un codice OTP tramite un'app di autenticazione" totpDescription: "Inserisci un codice OTP tramite un'app di autenticazione"
moderator: "Moderatore" moderator: "Moderatore"
@ -558,7 +560,7 @@ disablePagesScript: "Disabilita AiScript nelle pagine"
updateRemoteUser: "Aggiornare le informazioni di utente remot@" updateRemoteUser: "Aggiornare le informazioni di utente remot@"
deleteAllFiles: "Elimina tutti i file" deleteAllFiles: "Elimina tutti i file"
deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?" deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?"
removeAllFollowing: "Cancella tutti i follows" removeAllFollowing: "Annulla tutti i follow"
removeAllFollowingDescription: "Cancella tutti i follows del server {host}. Per favore, esegui se, ad esempio, l'istanza non esiste più." removeAllFollowingDescription: "Cancella tutti i follows del server {host}. Per favore, esegui se, ad esempio, l'istanza non esiste più."
userSuspended: "L'utente è in sospensione" userSuspended: "L'utente è in sospensione"
userSilenced: "L'utente è silenziat@." userSilenced: "L'utente è silenziat@."
@ -653,7 +655,8 @@ fileIdOrUrl: "ID o URL del file"
behavior: "Comportamento" behavior: "Comportamento"
sample: "Esempio" sample: "Esempio"
abuseReports: "Segnalazioni" abuseReports: "Segnalazioni"
reportAbuse: "Segnalazioni" reportAbuse: "Segnala"
reportAbuseRenote: "Segnala la Rinota"
reportAbuseOf: "Segnala {name}" reportAbuseOf: "Segnala {name}"
fillAbuseReportDescription: "Per favore, spiegaci il motivo della segnalazione. Se riguarda una Nota precisa, indica anche l'indirizzo URL." fillAbuseReportDescription: "Per favore, spiegaci il motivo della segnalazione. Se riguarda una Nota precisa, indica anche l'indirizzo URL."
abuseReported: "La segnalazione è stata inviata. Grazie." abuseReported: "La segnalazione è stata inviata. Grazie."
@ -681,7 +684,7 @@ createNewClip: "Crea una Clip"
unclip: "Togli Nota dalla Clip" unclip: "Togli Nota dalla Clip"
confirmToUnclipAlreadyClippedNote: "Questa nota è già inclusa in \"{name}\". Si desidera escludere la nota?" confirmToUnclipAlreadyClippedNote: "Questa nota è già inclusa in \"{name}\". Si desidera escludere la nota?"
public: "Pubblica" public: "Pubblica"
private: "Invisibile" private: "Privato"
i18nInfo: "Misskey è tradotto in diverse lingue da volontari. Anche tu puoi contribuire su {link}." i18nInfo: "Misskey è tradotto in diverse lingue da volontari. Anche tu puoi contribuire su {link}."
manageAccessTokens: "Gestisci token di accesso" manageAccessTokens: "Gestisci token di accesso"
accountInfo: "Informazioni profilo" accountInfo: "Informazioni profilo"
@ -762,7 +765,7 @@ editCode: "Modifica codice"
apply: "Applica" apply: "Applica"
receiveAnnouncementFromInstance: "Ricevi i messaggi informativi dall'istanza" receiveAnnouncementFromInstance: "Ricevi i messaggi informativi dall'istanza"
emailNotification: "Eventi per notifiche via mail" emailNotification: "Eventi per notifiche via mail"
publish: "Pubblico" publish: "Pubblicare"
inChannelSearch: "Cerca in canale" inChannelSearch: "Cerca in canale"
useReactionPickerForContextMenu: "Cliccare sul tasto destro per aprire il pannello di reazioni" useReactionPickerForContextMenu: "Cliccare sul tasto destro per aprire il pannello di reazioni"
typingUsers: "{users} sta(nno) scrivendo" typingUsers: "{users} sta(nno) scrivendo"
@ -842,8 +845,8 @@ pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente" lastCommunication: "La comunicazione più recente"
resolved: "Risolto" resolved: "Risolto"
unresolved: "Non risolto" unresolved: "Non risolto"
breakFollow: "Non seguire" breakFollow: "Non farti più seguire"
breakFollowConfirm: "Vuoi davvero togliere follower?" breakFollowConfirm: "Vuoi davvero smettere di seguire questo profilo?"
itsOn: "Abilitato" itsOn: "Abilitato"
itsOff: "Disabilitato" itsOff: "Disabilitato"
on: "Acceso" on: "Acceso"
@ -1097,7 +1100,7 @@ doYouAgree: "Accetti le condizioni?"
beSureToReadThisAsItIsImportant: "Si prega di leggere attentamente perché è importante." beSureToReadThisAsItIsImportant: "Si prega di leggere attentamente perché è importante."
iHaveReadXCarefullyAndAgree: "Dichiaro di aver letto attentamente \"{x}\" e accettarne le condizioni." iHaveReadXCarefullyAndAgree: "Dichiaro di aver letto attentamente \"{x}\" e accettarne le condizioni."
dialog: "Dialogo" dialog: "Dialogo"
icon: "Foto del profilo" icon: "Ritratto"
forYou: "Per te" forYou: "Per te"
currentAnnouncements: "Annunci attuali" currentAnnouncements: "Annunci attuali"
pastAnnouncements: "Annunci precedenti" pastAnnouncements: "Annunci precedenti"
@ -1468,7 +1471,7 @@ _emailUnavailable:
mx: "Server email non corretto" mx: "Server email non corretto"
smtp: "Il server email non risponde" smtp: "Il server email non risponde"
_ffVisibility: _ffVisibility:
public: "Pubblico" public: "Pubblica"
followers: "Mostra solo ai follower" followers: "Mostra solo ai follower"
private: "Invisibile" private: "Invisibile"
_signup: _signup:
@ -1696,9 +1699,10 @@ _2fa:
step1: "Innanzitutto, installare sul dispositivo un'applicazione di autenticazione come {a} o {b}." step1: "Innanzitutto, installare sul dispositivo un'applicazione di autenticazione come {a} o {b}."
step2: "Quindi, scansionare il codice QR visualizzato con l'app." step2: "Quindi, scansionare il codice QR visualizzato con l'app."
step2Click: "Cliccando sul codice QR, puoi registrarlo con l'app di autenticazione o il portachiavi installato sul tuo dispositivo." step2Click: "Cliccando sul codice QR, puoi registrarlo con l'app di autenticazione o il portachiavi installato sul tuo dispositivo."
step2Url: "Nell'applicazione desktop inserire il seguente URL: " step2Uri: "Inserisci il seguente URL se desideri utilizzare una App per PC"
step3Title: "Inserisci il codice di verifica" step3Title: "Inserisci il codice di verifica"
step3: "Inserite il token visualizzato nell'app e il gioco è fatto." step3: "Inserite il token visualizzato nell'app e il gioco è fatto."
setupCompleted: "Impostazione completata"
step4: "D'ora in poi, quando si accede, si inserisce il token nello stesso modo." step4: "D'ora in poi, quando si accede, si inserisce il token nello stesso modo."
securityKeyNotSupported: "Il tuo browser non supporta le chiavi di sicurezza." securityKeyNotSupported: "Il tuo browser non supporta le chiavi di sicurezza."
registerTOTPBeforeKey: "Ti occorre un'app di autenticazione con OTP, prima di registrare la chiave di sicurezza." registerTOTPBeforeKey: "Ti occorre un'app di autenticazione con OTP, prima di registrare la chiave di sicurezza."
@ -1714,6 +1718,11 @@ _2fa:
renewTOTPConfirm: "I codici di verifica nelle app di autenticazione esistenti smetteranno di funzionare" renewTOTPConfirm: "I codici di verifica nelle app di autenticazione esistenti smetteranno di funzionare"
renewTOTPOk: "Ripristina" renewTOTPOk: "Ripristina"
renewTOTPCancel: "No grazie" renewTOTPCancel: "No grazie"
checkBackupCodesBeforeCloseThisWizard: "Prima di chiudere questa procedura guidata, salva i tuoi codici usa-e-getta in un posto sicuro."
backupCodes: "Codici usa-e-getta"
backupCodesDescription: "Puoi usare questi codici usa-e-getta per ottenere l'accesso al tuo profilo in caso sia impossibile usare l'App col codice OTP. Salvali in un posto sicuro."
backupCodeUsedWarning: "È stato usato un codice usa-e-getta. Per favore, riconfigura l'autenticazione a due fattori il prima possibile, nel caso la configurazione precedente abbia smesso di funzionare."
backupCodesExhaustedWarning: "Hai esaurito i codici usa-e-getta. Se l'App che genera il codice OTP non è più disponibile, non potrai più accedere al tuo profilo. Ripeti la configurazione per l'autenticazione a due fattori."
_permissions: _permissions:
"read:account": "Visualizza le informazioni sul profilo" "read:account": "Visualizza le informazioni sul profilo"
"write:account": "Modifica le informazioni sul profilo" "write:account": "Modifica le informazioni sul profilo"
@ -1747,6 +1756,10 @@ _permissions:
"write:gallery": "Gestione della galleria" "write:gallery": "Gestione della galleria"
"read:gallery-likes": "Visualizza i contenuti della galleria." "read:gallery-likes": "Visualizza i contenuti della galleria."
"write:gallery-likes": "Manipolazione dei \"Mi piace\" della galleria." "write:gallery-likes": "Manipolazione dei \"Mi piace\" della galleria."
"read:flash": "Visualizza Play"
"write:flash": "Modifica Play"
"read:flash-likes": "Visualizza lista di Play piaciuti"
"write:flash-likes": "Modifica lista di Play piaciuti"
_auth: _auth:
shareAccessTitle: "Permessi dell'applicazione" shareAccessTitle: "Permessi dell'applicazione"
shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?" shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?"
@ -2017,6 +2030,8 @@ _deck:
introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo." introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo."
widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità" widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità"
useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice" useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice"
usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima"
flexible: "Larghezza flessibile"
_columns: _columns:
main: "Principale" main: "Principale"
widgets: "Riquadri" widgets: "Riquadri"

View File

@ -45,6 +45,7 @@ pin: "ピン留め"
unpin: "ピン留め解除" unpin: "ピン留め解除"
copyContent: "内容をコピー" copyContent: "内容をコピー"
copyLink: "リンクをコピー" copyLink: "リンクをコピー"
copyLinkRenote: "Renoteのリンクをコピー"
delete: "削除" delete: "削除"
deleteAndEdit: "削除して編集" deleteAndEdit: "削除して編集"
deleteAndEditConfirm: "このートを削除してもう一度編集しますかこのートへのリアクション、Renote、返信も全て削除されます。" deleteAndEditConfirm: "このートを削除してもう一度編集しますかこのートへのリアクション、Renote、返信も全て削除されます。"
@ -655,6 +656,7 @@ behavior: "動作"
sample: "サンプル" sample: "サンプル"
abuseReports: "通報" abuseReports: "通報"
reportAbuse: "通報" reportAbuse: "通報"
reportAbuseRenote: "Renoteを通報"
reportAbuseOf: "{name}を通報する" reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のートがある場合はそのURLも記入してください。" fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のートがある場合はそのURLも記入してください。"
abuseReported: "内容が送信されました。ご報告ありがとうございました。" abuseReported: "内容が送信されました。ご報告ありがとうございました。"
@ -1021,7 +1023,7 @@ retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大するこ
enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きく表示" reactionsDisplaySize: "リアクションの表示サイズ"
noteIdOrUrl: "ートIDまたはURL" noteIdOrUrl: "ートIDまたはURL"
video: "動画" video: "動画"
videos: "動画" videos: "動画"
@ -1103,6 +1105,7 @@ forYou: "あなたへ"
currentAnnouncements: "現在のお知らせ" currentAnnouncements: "現在のお知らせ"
pastAnnouncements: "過去のお知らせ" pastAnnouncements: "過去のお知らせ"
youHaveUnreadAnnouncements: "未読のお知らせがあります。" youHaveUnreadAnnouncements: "未読のお知らせがあります。"
useSecurityKey: "ブラウザまたはデバイスの指示に従って、セキュリティキーまたはパスキーを使用してください。"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
@ -1738,7 +1741,6 @@ _2fa:
securityKeyNotSupported: "お使いのブラウザはセキュリティキーに対応していません。" securityKeyNotSupported: "お使いのブラウザはセキュリティキーに対応していません。"
registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。" registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。"
securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。"
chromePasskeyNotSupported: "Chromeのパスキーは現在サポートしていません。"
registerSecurityKey: "セキュリティキー・パスキーを登録する" registerSecurityKey: "セキュリティキー・パスキーを登録する"
securityKeyName: "キーの名前を入力" securityKeyName: "キーの名前を入力"
tapSecurityKey: "ブラウザの指示に従い、セキュリティキーやパスキーを登録してください" tapSecurityKey: "ブラウザの指示に従い、セキュリティキーやパスキーを登録してください"
@ -2045,6 +2047,10 @@ _notification:
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしました" emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得" achievementEarned: "実績を獲得"
testNotification: "通知テスト"
checkNotificationBehavior: "通知の表示を確かめる"
sendTestNotification: "テスト通知を送信する"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
_types: _types:
all: "すべて" all: "すべて"
@ -2083,6 +2089,8 @@ _deck:
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。" introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
flexible: "幅を自動調整"
_columns: _columns:
main: "メイン" main: "メイン"

View File

@ -1099,9 +1099,18 @@ iHaveReadXCarefullyAndAgree: "「{x}」の内容をよう読んで、同意す
dialog: "ダイアログ" dialog: "ダイアログ"
icon: "アイコン" icon: "アイコン"
forYou: "あんたへ" forYou: "あんたへ"
currentAnnouncements: "現在のお知らせやで"
pastAnnouncements: "過去のお知らせやで"
youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんやろ。" youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんやろ。"
_announcement: _announcement:
forExistingUsers: "もうおるユーザーのみ"
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
needConfirmationToRead: "既読にするのに確認が必要やで"
needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象にもなりません。"
end: "お知らせを終了"
tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討した方がええよ。"
readConfirmTitle: "既読にしてええんやな?" readConfirmTitle: "既読にしてええんやな?"
readConfirmText: "「{title}」の内容を読み、既読にします。"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウント作り終わったで。" accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。" letsStartAccountSetup: "アカウントの初期設定をしよか。"
@ -1687,7 +1696,6 @@ _2fa:
step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。"
step2: "次に、ここにあるQRコードをアプリでスキャンしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな。"
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
step2Url: "デスクトップアプリやったら次のURLを入力してや:"
step3Title: "確認コードを入れてーや" step3Title: "確認コードを入れてーや"
step3: "アプリに表示されているトークンを入力して終わりや。" step3: "アプリに表示されているトークンを入力して終わりや。"
step4: "これからログインするときも、同じようにトークンを入力するんやで" step4: "これからログインするときも、同じようにトークンを入力するんやで"
@ -1738,6 +1746,10 @@ _permissions:
"write:gallery": "ギャラリーを操作するで" "write:gallery": "ギャラリーを操作するで"
"read:gallery-likes": "ギャラリーのいいねを見るで" "read:gallery-likes": "ギャラリーのいいねを見るで"
"write:gallery-likes": "ギャラリーのいいねを操作するで" "write:gallery-likes": "ギャラリーのいいねを操作するで"
"read:flash": "Playを見る"
"write:flash": "Playを操作する"
"read:flash-likes": "Playのええやんを見る"
"write:flash-likes": "Playのええやんを見る"
_auth: _auth:
shareAccessTitle: "アプリへのアクセス許してやったらどうや" shareAccessTitle: "アプリへのアクセス許してやったらどうや"
shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?" shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?"

View File

@ -1 +1,3 @@
--- ---
_lang_: "la .lojban."
headlineMisskey: "lo se tcana noi jorne fi loi notci"

View File

@ -2,20 +2,20 @@
_lang_: "한국어" _lang_: "한국어"
headlineMisskey: "노트로 연결되는 네트워크" headlineMisskey: "노트로 연결되는 네트워크"
introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀" introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀"
poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼 <b>Misskey</b>를 사용한 서버 가운데 하나입니다." poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼<b>Misskey</b>를 사용한 서비스(Misskey 인스턴스라고 불립니다) 중 하나입니다."
monthAndDay: "{month}월 {day}일" monthAndDay: "{month}월 {day}일"
search: "검색" search: "검색"
notifications: "알림" notifications: "알림"
username: "유저명" username: "유저명"
password: "비밀번호" password: "비밀번호"
forgotPassword: "비밀번호 재설정" forgotPassword: "비밀번호 재설정"
fetchingAsApObject: "연합에 조회 중" fetchingAsApObject: "연합에 조회 중"
ok: "확인" ok: "확인"
gotIt: "알겠어요" gotIt: "알겠어요"
cancel: "취소" cancel: "취소"
noThankYou: "나중에" noThankYou: "나중에"
enterUsername: "유저명 입력" enterUsername: "유저명 입력"
renotedBy: "{user}님 리노트" renotedBy: "{user}님 리노트"
noNotes: "노트가 없습니다" noNotes: "노트가 없습니다"
noNotifications: "표시할 알림이 없습니다" noNotifications: "표시할 알림이 없습니다"
instance: "서버" instance: "서버"
@ -155,7 +155,7 @@ emojiUrl: "이모지 URL"
addEmoji: "이모지 추가" addEmoji: "이모지 추가"
settingGuide: "추천 설정" settingGuide: "추천 설정"
cacheRemoteFiles: "리모트 파일을 캐시" cacheRemoteFiles: "리모트 파일을 캐시"
cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다." cacheRemoteFilesDescription: "이 설정을 활성화하면 리모트 파일을 이 서버의 스토리지에 캐시합니다. 미디어의 표시가 빨라지지만, 서버의 저장 용량을 크게 소모합니다. 리모트 유저의 미디어를 얼마나 보관할 지는 역할의 드라이브 용량 제한에 따라 결정되며, 정해진 용량을 넘길 경우 오래된 파일부터 차례대로 삭제한 뒤 링크로 전환합니다. \n비활성화하면 리모트 파일을 직접 링크하며, 이 경우 이미지 썸네일 생성 및 유저 프라이버시 보호를 위해 default.yml에서 proxyRemoteFiles를 true로 설정하는 것을 권장합니다."
youCanCleanRemoteFilesCache: "파일 관리 화면의 🗑️ 버튼을 눌러 모든 캐시를 삭제할 수 있습니다." youCanCleanRemoteFilesCache: "파일 관리 화면의 🗑️ 버튼을 눌러 모든 캐시를 삭제할 수 있습니다."
cacheRemoteSensitiveFiles: "리모트의 민감한 파일을 캐시" cacheRemoteSensitiveFiles: "리모트의 민감한 파일을 캐시"
cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모트의 민감한 파일은 캐시하지 않고 리모트에서 직접 가져오도록 합니다." cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모트의 민감한 파일은 캐시하지 않고 리모트에서 직접 가져오도록 합니다."
@ -411,6 +411,7 @@ aboutMisskey: "Misskey에 대하여"
administrator: "관리자" administrator: "관리자"
token: "토큰" token: "토큰"
2fa: "2단계 인증" 2fa: "2단계 인증"
setupOf2fa: "2단계 인증 설정"
totp: "인증 앱" totp: "인증 앱"
totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력" totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
moderator: "모더레이터" moderator: "모더레이터"
@ -878,7 +879,7 @@ numberOfColumn: "한 줄에 보일 리액션의 수"
searchByGoogle: "검색" searchByGoogle: "검색"
instanceDefaultLightTheme: "서버 기본 라이트 테마" instanceDefaultLightTheme: "서버 기본 라이트 테마"
instanceDefaultDarkTheme: "서버 기본 다크 테마" instanceDefaultDarkTheme: "서버 기본 다크 테마"
instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요." instanceDefaultThemeDescription: "객체 형식({}로 감싼 형태)의 테마 코드를 입력해 주세요."
mutePeriod: "뮤트할 기간" mutePeriod: "뮤트할 기간"
period: "기간" period: "기간"
indefinitely: "무기한" indefinitely: "무기한"
@ -1696,9 +1697,10 @@ _2fa:
step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다." step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다."
step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다." step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다."
step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있습니다." step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있습니다."
step2Url: "데스크톱 앱에서는 다음 URL을 입력하세요:" step2Uri: "데스크톱 앱을 사용하려면 다음 URI를 입력하십시오"
step3Title: "인증 코드 입력" step3Title: "인증 코드 입력"
step3: "앱에 표시된 토큰을 입력하시면 완료됩니다." step3: "앱에 표시된 토큰을 입력하시면 완료됩니다."
setupCompleted: "설정 완료했습니다"
step4: "다음 로그인부터는 토큰을 입력해야 합니다." step4: "다음 로그인부터는 토큰을 입력해야 합니다."
securityKeyNotSupported: "이 브라우저는 보안 키를 지원하지 않습니다." securityKeyNotSupported: "이 브라우저는 보안 키를 지원하지 않습니다."
registerTOTPBeforeKey: "보안 키 또는 패스키를 등록하려면 인증 앱을 등록하십시오." registerTOTPBeforeKey: "보안 키 또는 패스키를 등록하려면 인증 앱을 등록하십시오."
@ -1714,6 +1716,11 @@ _2fa:
renewTOTPConfirm: "기존에 등록되어 있던 인증 키는 사용하지 못하게 됩니다." renewTOTPConfirm: "기존에 등록되어 있던 인증 키는 사용하지 못하게 됩니다."
renewTOTPOk: "재설정" renewTOTPOk: "재설정"
renewTOTPCancel: "취소" renewTOTPCancel: "취소"
checkBackupCodesBeforeCloseThisWizard: "이 위자드를 닫기 전에 아래 백업 코드를 확인하십시오"
backupCodes: "백업 코드"
backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다."
backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오."
backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요."
_permissions: _permissions:
"read:account": "계정의 정보를 봅니다" "read:account": "계정의 정보를 봅니다"
"write:account": "계정의 정보를 변경합니다" "write:account": "계정의 정보를 변경합니다"
@ -1747,6 +1754,10 @@ _permissions:
"write:gallery": "갤러리를 추가하거나 삭제합니다" "write:gallery": "갤러리를 추가하거나 삭제합니다"
"read:gallery-likes": "갤러리의 좋아요를 확인합니다" "read:gallery-likes": "갤러리의 좋아요를 확인합니다"
"write:gallery-likes": "갤러리에 좋아요를 추가하거나 취소합니다" "write:gallery-likes": "갤러리에 좋아요를 추가하거나 취소합니다"
"read:flash": "Play를 봅니다"
"write:flash": "Play를 조작합니다"
"read:flash-likes": "Play의 좋아요를 봅니다"
"write:flash-likes": "Play의 좋아요를 조작합니다"
_auth: _auth:
shareAccessTitle: "어플리케이션의 접근 허가" shareAccessTitle: "어플리케이션의 접근 허가"
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?" shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?"

View File

@ -45,6 +45,7 @@ pin: "Fixar no perfil"
unpin: "Desafixar do perfil" unpin: "Desafixar do perfil"
copyContent: "Copiar conteúdos" copyContent: "Copiar conteúdos"
copyLink: "Copiar link" copyLink: "Copiar link"
copyLinkRenote: "Copiar o link da repostagem"
delete: "Excluir" delete: "Excluir"
deleteAndEdit: "Excluir e editar" deleteAndEdit: "Excluir e editar"
deleteAndEditConfirm: "Deseja excluir esta nota e editá-la novamente? Todas as reações, compartilhamentos e respostas a esta nota também serão excluídas." deleteAndEditConfirm: "Deseja excluir esta nota e editá-la novamente? Todas as reações, compartilhamentos e respostas a esta nota também serão excluídas."
@ -654,6 +655,7 @@ behavior: "Comportamento"
sample: "Exemplo" sample: "Exemplo"
abuseReports: "Denúncias" abuseReports: "Denúncias"
reportAbuse: "Denúncias" reportAbuse: "Denúncias"
reportAbuseRenote: "Reportar repostagem"
reportAbuseOf: "Denunciar {name}" reportAbuseOf: "Denunciar {name}"
fillAbuseReportDescription: "Por favor, forneça detalhes sobre o motivo da denúncia. Se houver uma nota específica envolvida, inclua também a URL dela." fillAbuseReportDescription: "Por favor, forneça detalhes sobre o motivo da denúncia. Se houver uma nota específica envolvida, inclua também a URL dela."
abuseReported: "Denúncia enviada. Obrigado por sua ajuda." abuseReported: "Denúncia enviada. Obrigado por sua ajuda."
@ -916,18 +918,43 @@ statusbar: "Barra de status"
pleaseSelect: "Por favor, selecione." pleaseSelect: "Por favor, selecione."
reverse: "Inversão" reverse: "Inversão"
colored: "Colorido" colored: "Colorido"
refreshInterval: "Intervalo de atualização"
type: "Tipo"
speed: "Velocidade"
slow: "Lento"
fast: "Rápido"
sensitiveMediaDetection: "Detecção de conteúdo sensível"
localOnly: "Apenas local"
remoteOnly: "Apenas remoto"
cannotUploadBecauseExceedsFileSizeLimit: "Não é possível realizar o upload deste arquivo porque ele excede o tamanho máximo permitido."
beta: "Beta"
enableAutoSensitive: "Marcar automaticamente como conteúdo sensível"
enableAutoSensitiveDescription: "Quando disponível, a marcação de mídia sensível será automaticamente atribuído ao conteúdo de mídia usando aprendizado de máquina. Mesmo que você desative essa função, em alguns servidores, isso pode ser configurado automaticamente." enableAutoSensitiveDescription: "Quando disponível, a marcação de mídia sensível será automaticamente atribuído ao conteúdo de mídia usando aprendizado de máquina. Mesmo que você desative essa função, em alguns servidores, isso pode ser configurado automaticamente."
activeEmailValidationDescription: "A validação do endereço de e-mail do usuário será realizada de forma mais rigorosa, considerando se é um endereço descartável ou se é possível realizar comunicação efetiva. Se desativado, apenas a validade do formato do endereço será verificada como uma sequência de caracteres." activeEmailValidationDescription: "A validação do endereço de e-mail do usuário será realizada de forma mais rigorosa, considerando se é um endereço descartável ou se é possível realizar comunicação efetiva. Se desativado, apenas a validade do formato do endereço será verificada como uma sequência de caracteres."
shuffle: "Aleatório"
account: "Contas" account: "Contas"
move: "Mover"
pushNotification: "Notificações Push"
subscribePushNotification: "Ativar notificações push"
unsubscribePushNotification: "Desativar notificações push"
windowMinimize: "Minimizar"
windowRestore: "Restaurar"
caption: "legenda"
tools: "Ferramentas"
like: "Curtir" like: "Curtir"
unlike: "Remover curtida" unlike: "Remover curtida"
numberOfLikes: "Número de curtidas" numberOfLikes: "Número de curtidas"
show: "Visualizar" show: "Visualizar"
neverShow: "Não exibir novamente"
remindMeLater: "Lembrar mais tarde"
didYouLikeMisskey: "Você gostou do Misskey?" didYouLikeMisskey: "Você gostou do Misskey?"
pleaseDonate: "O Misskey é um software gratuito utilizado por {host}. Para que possamos continuar o desenvolvimento, pedimos que considerem fazer doações. A sua contribuição é muito importante!" pleaseDonate: "O Misskey é um software gratuito utilizado por {host}. Para que possamos continuar o desenvolvimento, pedimos que considerem fazer doações. A sua contribuição é muito importante!"
roles: "Cargos" roles: "Cargos"
role: "Cargo" role: "Cargo"
noRole: "Nenhum cargo" noRole: "Nenhum cargo"
normalUser: "Usuários padrão"
undefined: "Indefinido"
assign: "Atribuir"
unassign: "Remover" unassign: "Remover"
color: "Cor" color: "Cor"
manageCustomEmojis: "Gerenciar Emojis customizados" manageCustomEmojis: "Gerenciar Emojis customizados"
@ -947,7 +974,7 @@ thisPostMayBeAnnoying: "Esta nota pode incomodar outras pessoas."
thisPostMayBeAnnoyingHome: "Postar na linha do tempo inicial" thisPostMayBeAnnoyingHome: "Postar na linha do tempo inicial"
thisPostMayBeAnnoyingCancel: "Cancelar" thisPostMayBeAnnoyingCancel: "Cancelar"
thisPostMayBeAnnoyingIgnore: "Postar mesmo assim" thisPostMayBeAnnoyingIgnore: "Postar mesmo assim"
collapseRenotes: "Ocultar Renotes já visualizadas" collapseRenotes: "Ocultar repostagens já visualizadas"
internalServerError: "Erro interno de servidor" internalServerError: "Erro interno de servidor"
emailNotSupported: "O envio de e-mails não é suportado nesta instância" emailNotSupported: "O envio de e-mails não é suportado nesta instância"
likeOnly: "Apenas curtidas" likeOnly: "Apenas curtidas"
@ -957,8 +984,19 @@ rolesAssignedToMe: "Cargos atribuídos a mim"
unfavoriteConfirm: "Deseja realmente remover dos favoritos?" unfavoriteConfirm: "Deseja realmente remover dos favoritos?"
drivecleaner: "Limpeza do drive" drivecleaner: "Limpeza do drive"
retryAllQueuesConfirmTitle: "Gostaria de tentar novamente agora?" retryAllQueuesConfirmTitle: "Gostaria de tentar novamente agora?"
reactionsList: "Reações"
renotesList: "Repostagens"
leftTop: "Superior esquerdo"
rightTop: "Superior direito"
leftBottom: "Inferior esquerdo"
rightBottom: "Inferior direito"
vertical: "Vertical"
horizontal: "Exibir painel lateral inteiro" horizontal: "Exibir painel lateral inteiro"
position: "Posição"
serverRules: "Regras do servidor"
continue: "Continuar"
preservedUsernamesDescription: "Liste os nomes de usuário que deseja reservar, separando-os por quebras de linha. Os nomes de usuário especificados aqui não poderão ser utilizados durante a criação de contas. No entanto, esta restrição não se aplica quando a conta é criada por um administrador. Além disso, as contas que já existem não serão afetadas." preservedUsernamesDescription: "Liste os nomes de usuário que deseja reservar, separando-os por quebras de linha. Os nomes de usuário especificados aqui não poderão ser utilizados durante a criação de contas. No entanto, esta restrição não se aplica quando a conta é criada por um administrador. Além disso, as contas que já existem não serão afetadas."
archive: "Arquivo"
channelArchiveConfirmTitle: "Deseja realmente arquivar {name}?" channelArchiveConfirmTitle: "Deseja realmente arquivar {name}?"
youFollowing: "Seguindo" youFollowing: "Seguindo"
preventAiLearningDescription: "Solicita-se que o conteúdo de notas e imagens enviadas não seja usado como objeto de aprendizado por sistemas externos de geração de texto ou imagens. Isso é alcançado incluindo a flag 'noai' na resposta HTML. No entanto, o cumprimento dessa solicitação depende do próprio sistema de IA, portanto, não é garantia total de prevenção de aprendizado." preventAiLearningDescription: "Solicita-se que o conteúdo de notas e imagens enviadas não seja usado como objeto de aprendizado por sistemas externos de geração de texto ou imagens. Isso é alcançado incluindo a flag 'noai' na resposta HTML. No entanto, o cumprimento dessa solicitação depende do próprio sistema de IA, portanto, não é garantia total de prevenção de aprendizado."
@ -1262,6 +1300,8 @@ _menuDisplay:
sideFull: "Exibir painel lateral inteiro" sideFull: "Exibir painel lateral inteiro"
top: "Exibir barra superior" top: "Exibir barra superior"
hide: "Ocultar" hide: "Ocultar"
_instanceMute:
instanceMuteDescription: "Todas as notas e repostagens do servidor configurado serão silenciados, incluindo respostas aos usuários do servidor mutado."
_theme: _theme:
description: "Descrição" description: "Descrição"
alpha: "Opacidade" alpha: "Opacidade"
@ -1396,6 +1436,7 @@ _notification:
youGotMention: "{name} te mencionou" youGotMention: "{name} te mencionou"
youGotReply: "{name} te respondeu" youGotReply: "{name} te respondeu"
youGotQuote: "{name} te citou" youGotQuote: "{name} te citou"
youRenoted: "Repostagens de {name}"
youWereFollowed: "Você tem um novo seguidor" youWereFollowed: "Você tem um novo seguidor"
youReceivedFollowRequest: "Você recebeu um pedido de seguidor" youReceivedFollowRequest: "Você recebeu um pedido de seguidor"
yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito" yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito"
@ -1448,3 +1489,4 @@ _webhookSettings:
_events: _events:
follow: "Quando seguindo um usuário" follow: "Quando seguindo um usuário"
followed: "Quando sendo seguido" followed: "Quando sendo seguido"
renote: "Quando repostado"

View File

@ -1610,7 +1610,6 @@ _2fa:
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
step2Click: "Нажав на QR-код, вы можете зарегистрироваться с помощью приложения для аутентификации или брелка для ключей, установленного на вашем устройстве." step2Click: "Нажав на QR-код, вы можете зарегистрироваться с помощью приложения для аутентификации или брелка для ключей, установленного на вашем устройстве."
step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):"
step3Title: "Введите проверочный код" step3Title: "Введите проверочный код"
step3: "И наконец, введите код, который покажет приложение." step3: "И наконец, введите код, который покажет приложение."
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом." step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."

View File

@ -1149,7 +1149,6 @@ _2fa:
alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie." alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie."
step1: "Najprv si nainštalujte autentifikačnú aplikáciu (napríklad {a} alebo {b}) na svoje zariadenie." step1: "Najprv si nainštalujte autentifikačnú aplikáciu (napríklad {a} alebo {b}) na svoje zariadenie."
step2: "Potom, naskenujte QR kód zobrazený na obrazovke." step2: "Potom, naskenujte QR kód zobrazený na obrazovke."
step2Url: "Do aplikácie zadajte nasledujúcu URL adresu:"
step3: "Nastavenie dokončíte zadaním tokenu z vašej aplikácie." step3: "Nastavenie dokončíte zadaním tokenu z vašej aplikácie."
step4: "Od teraz, všetky ďalšie prihlásenia budú vyžadovať prihlasovací token." step4: "Od teraz, všetky ďalšie prihlásenia budú vyžadovať prihlasovací token."
securityKeyInfo: "Okrem odtlačku prsta alebo PIN autentifikácie si môžete nastaviť autentifikáciu cez hardvérový bezpečnostný kľúč podporujúci FIDO2 a tak ešte viac zabezpečiť svoj účet." securityKeyInfo: "Okrem odtlačku prsta alebo PIN autentifikácie si môžete nastaviť autentifikáciu cez hardvérový bezpečnostný kľúč podporujúci FIDO2 a tak ešte viac zabezpečiť svoj účet."

View File

@ -1691,7 +1691,6 @@ _2fa:
step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ" step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ"
step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้" step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้"
step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้" step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้"
step2Url: "คุณยังสามารถป้อนบน URL นี้หากคุณใช้โปรแกรมเดสก์ท็อป:"
step3Title: "ป้อนรหัสยืนยัน" step3Title: "ป้อนรหัสยืนยัน"
step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า" step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า"
step4: "นับจากนี้เป็นต้นไปการพยายามเข้าสู่ระบบในอนาคตนั้น อาจจะต้องขอโทเค็นในการเข้าสู่ระบบดังกล่าว" step4: "นับจากนี้เป็นต้นไปการพยายามเข้าสู่ระบบในอนาคตนั้น อาจจะต้องขอโทเค็นในการเข้าสู่ระบบดังกล่าว"

View File

@ -1337,7 +1337,6 @@ _2fa:
alreadyRegistered: "Двофакторна автентифікація вже налаштована." alreadyRegistered: "Двофакторна автентифікація вже налаштована."
step1: "Спершу встановіть на свій пристрій програму автентифікації (наприклад {a} або {b})." step1: "Спершу встановіть на свій пристрій програму автентифікації (наприклад {a} або {b})."
step2: "Потім відскануйте QR-код, який відображається на цьому екрані." step2: "Потім відскануйте QR-код, який відображається на цьому екрані."
step2Url: "Ви також можете ввести цю URL-адресу, якщо використовуєте програму для ПК:"
step3: "Щоб завершити налаштування, введіть токен, наданий вашою програмою." step3: "Щоб завершити налаштування, введіть токен, наданий вашою програмою."
step4: "Відтепер будь-які майбутні спроби входу вимагатимуть такого токена." step4: "Відтепер будь-які майбутні спроби входу вимагатимуть такого токена."
renewTOTPCancel: "Не зараз" renewTOTPCancel: "Не зараз"

View File

@ -699,6 +699,7 @@ myTheme: "Mening rang sxemam"
backgroundColor: "Fon" backgroundColor: "Fon"
accentColor: "Urg'u" accentColor: "Urg'u"
textColor: "Matn" textColor: "Matn"
saveAs: "Boshqacha saqlash"
advanced: "Murakkab" advanced: "Murakkab"
advancedSettings: "Qo'shimcha sozlashlar" advancedSettings: "Qo'shimcha sozlashlar"
value: "Qiymati" value: "Qiymati"
@ -725,6 +726,7 @@ inChannelSearch: "Kanal qidirish"
useReactionPickerForContextMenu: "kontekst menyusi uchun reaktsiya tanlash vositasidan foydalaning" useReactionPickerForContextMenu: "kontekst menyusi uchun reaktsiya tanlash vositasidan foydalaning"
typingUsers: "{users} yozmoqda" typingUsers: "{users} yozmoqda"
jumpToSpecifiedDate: "Muayyan sanaga o'tish" jumpToSpecifiedDate: "Muayyan sanaga o'tish"
showingPastTimeline: "O'tgan vaqt jadvallarini ko'rsatish"
clear: "aniq" clear: "aniq"
markAllAsRead: "hammasini o'qilgan deb belgilang" markAllAsRead: "hammasini o'qilgan deb belgilang"
goBack: "qaytish" goBack: "qaytish"

View File

@ -1404,7 +1404,6 @@ _2fa:
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." 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." step2: "Sau đó, quét mã QR hiển thị trên màn hình này."
step2Click: "Quét mã QR trên ứng dụng xác thực (Authy, Google authenticator, v.v.)" step2Click: "Quét mã QR trên ứng dụng xác thực (Authy, Google authenticator, v.v.)"
step2Url: "Bạn cũng có thể nhập URL này nếu sử dụng một chương trình máy tính:"
step3Title: "Nhập mã xác thực" step3Title: "Nhập mã xác thực"
step3: "Nhập mã token do ứng dụng của bạn cung cấp để hoàn tất thiết lập." 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 đó." 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 đó."

View File

@ -45,6 +45,7 @@ pin: "置顶"
unpin: "取消置顶" unpin: "取消置顶"
copyContent: "复制内容" copyContent: "复制内容"
copyLink: "复制链接" copyLink: "复制链接"
copyLinkRenote: "复制转帖链接"
delete: "删除" delete: "删除"
deleteAndEdit: "删除并编辑" deleteAndEdit: "删除并编辑"
deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。" deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。"
@ -155,7 +156,7 @@ emojiUrl: "emoji 地址"
addEmoji: "添加表情符号" addEmoji: "添加表情符号"
settingGuide: "推荐配置" settingGuide: "推荐配置"
cacheRemoteFiles: "缓存远程文件" cacheRemoteFiles: "缓存远程文件"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。"
youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。" youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。"
cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件" cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件"
cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。" cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。"
@ -178,7 +179,7 @@ searchWith: "搜索:{q}"
youHaveNoLists: "列表为空" youHaveNoLists: "列表为空"
followConfirm: "你确定要关注 {name} 吗?" followConfirm: "你确定要关注 {name} 吗?"
proxyAccount: "代理账户" proxyAccount: "代理账户"
proxyAccountDescription: "代理账户是在某些情况下充当用户的远程关注者的账户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该服务器,因此将代之以代理账户。" proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。"
host: "主机名" host: "主机名"
selectUser: "选择用户" selectUser: "选择用户"
recipient: "收件人" recipient: "收件人"
@ -212,7 +213,7 @@ clearQueueConfirmText: "未送达的帖子将不会投递。 通常,您不需
clearCachedFiles: "清除缓存" clearCachedFiles: "清除缓存"
clearCachedFilesConfirm: "确定要清除缓存文件?" clearCachedFilesConfirm: "确定要清除缓存文件?"
blockedInstances: "被封锁的服务器" blockedInstances: "被封锁的服务器"
blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。" blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
muteAndBlock: "屏蔽/拉黑" muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户" mutedUsers: "已屏蔽用户"
blockedUsers: "已拉黑的用户" blockedUsers: "已拉黑的用户"
@ -411,6 +412,7 @@ aboutMisskey: "关于 Misskey"
administrator: "管理员" administrator: "管理员"
token: "Token (令牌)" token: "Token (令牌)"
2fa: "双因素认证" 2fa: "双因素认证"
setupOf2fa: "设置双因素认证"
totp: "身份验证应用" totp: "身份验证应用"
totpDescription: "使用认证应用输入一次性密码。" totpDescription: "使用认证应用输入一次性密码。"
moderator: "监察员" moderator: "监察员"
@ -654,6 +656,7 @@ behavior: "行为"
sample: "示例" sample: "示例"
abuseReports: "举报" abuseReports: "举报"
reportAbuse: "举报" reportAbuse: "举报"
reportAbuseRenote: "举报转帖"
reportAbuseOf: "举报 {name}" reportAbuseOf: "举报 {name}"
fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写 URL 地址。" fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写 URL 地址。"
abuseReported: "内容已发送。感谢您提交信息。" abuseReported: "内容已发送。感谢您提交信息。"
@ -1696,9 +1699,10 @@ _2fa:
step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。" step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。"
step2: "然后,扫描屏幕上显示的二维码。" step2: "然后,扫描屏幕上显示的二维码。"
step2Click: "通过点击二维码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册" step2Click: "通过点击二维码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册"
step2Url: "在桌面应用程序中输入以下 URL" step2Uri: "如果使用桌面应用程序的话,请输入下面的 URI"
step3Title: "输入验证码" step3Title: "输入验证码"
step3: "输入您的应用提供的动态口令以完成设置。" step3: "输入您的应用提供的动态口令以完成设置。"
setupCompleted: "设置完成"
step4: "从现在开始,任何登录操作都将要求您提供动态口令。" step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
securityKeyNotSupported: "您的浏览器不支持安全密钥。" securityKeyNotSupported: "您的浏览器不支持安全密钥。"
registerTOTPBeforeKey: "要注册安全密钥或 Passkey请先设置验证器应用程序。" registerTOTPBeforeKey: "要注册安全密钥或 Passkey请先设置验证器应用程序。"
@ -1714,6 +1718,11 @@ _2fa:
renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效" renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效"
renewTOTPOk: "重新配置" renewTOTPOk: "重新配置"
renewTOTPCancel: "不用,谢谢" renewTOTPCancel: "不用,谢谢"
checkBackupCodesBeforeCloseThisWizard: "在关闭此窗口前,请确认下面的备用代码"
backupCodes: "备用代码"
backupCodesDescription: "如果无法使用认证应用,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。"
backupCodeUsedWarning: "已使用备用代码。如果无法使用认证应用,请尽快重新设定。"
backupCodesExhaustedWarning: "已使用完所有的备用代码。如果无法使用认证应用,将无法再访问您的账户。请再次设定认证应用。"
_permissions: _permissions:
"read:account": "查看账户信息" "read:account": "查看账户信息"
"write:account": "更改帐户信息" "write:account": "更改帐户信息"
@ -1747,6 +1756,10 @@ _permissions:
"write:gallery": "操作图库" "write:gallery": "操作图库"
"read:gallery-likes": "读取喜欢的图片" "read:gallery-likes": "读取喜欢的图片"
"write:gallery-likes": "操作喜欢的图片" "write:gallery-likes": "操作喜欢的图片"
"read:flash": "查看 Play"
"write:flash": "编辑 Play"
"read:flash-likes": "查看 Play 的点赞"
"write:flash-likes": "编辑 Play 的点赞列表"
_auth: _auth:
shareAccessTitle: "应用程序授权许可" shareAccessTitle: "应用程序授权许可"
shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?" shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?"
@ -2017,6 +2030,8 @@ _deck:
introduction2: "您可以随时通过屏幕右侧的 + 来添加列" introduction2: "您可以随时通过屏幕右侧的 + 来添加列"
widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具" widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具"
useSimpleUiForNonRootPages: "用简易UI表示非根页面" useSimpleUiForNonRootPages: "用简易UI表示非根页面"
usedAsMinWidthWhenFlexible: "「自适应宽度」被启用的时候,这就是最小的宽度"
flexible: "自适应宽度"
_columns: _columns:
main: "主列" main: "主列"
widgets: "小工具" widgets: "小工具"

View File

@ -164,7 +164,7 @@ flagAsBotDescription: "標記本帳戶由程式控制,防止其他程式與本
flagAsCat: "此帳戶是一隻貓,喵~~~!!!" flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用時,時間除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。" flagShowTimelineRepliesDescription: "啟用時,時間除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。"
autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求" autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求"
addAccount: "新增帳戶" addAccount: "新增帳戶"
reloadAccountsList: "更新帳戶清單的資訊" reloadAccountsList: "更新帳戶清單的資訊"
@ -348,7 +348,7 @@ connectService: "已連結"
disconnectService: "已斷開 " disconnectService: "已斷開 "
enableLocalTimeline: "啟用本地時間軸" enableLocalTimeline: "啟用本地時間軸"
enableGlobalTimeline: "啟用全域時間軸" enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間功能,管理員和審查員仍可以繼續使用。" disablingTimelinesInfo: "為了方便,即使您關閉了時間功能,管理員和審查員仍可以繼續使用。"
registration: "註冊" registration: "註冊"
enableRegistration: "開放新使用者註冊" enableRegistration: "開放新使用者註冊"
invite: "邀請" invite: "邀請"
@ -411,6 +411,7 @@ aboutMisskey: "關於 Misskey"
administrator: "管理員" administrator: "管理員"
token: "權杖" token: "權杖"
2fa: "雙重驗證" 2fa: "雙重驗證"
setupOf2fa: "設定雙重驗證"
totp: "驗證應用程式" totp: "驗證應用程式"
totpDescription: "以驗證應用程式輸入一次性密碼" totpDescription: "以驗證應用程式輸入一次性密碼"
moderator: "審查員" moderator: "審查員"
@ -721,7 +722,7 @@ thisIsExperimentalFeature: "這是實驗性的功能。可能會有變更規格
developer: "開發者" developer: "開發者"
makeExplorable: "使自己的帳戶能夠在「探索」頁面中顯示" makeExplorable: "使自己的帳戶能夠在「探索」頁面中顯示"
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。" makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。"
showGapBetweenNotesInTimeline: "分開顯示時間上的貼文。" showGapBetweenNotesInTimeline: "分開顯示時間上的貼文。"
duplicate: "複製" duplicate: "複製"
left: "左" left: "左"
center: "置中" center: "置中"
@ -767,7 +768,7 @@ inChannelSearch: "頻道内搜尋"
useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄" useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄"
typingUsers: "{users}輸入中" typingUsers: "{users}輸入中"
jumpToSpecifiedDate: "跳轉到特定日期" jumpToSpecifiedDate: "跳轉到特定日期"
showingPastTimeline: "顯示過往的時間" showingPastTimeline: "顯示過往的時間"
clear: "清除" clear: "清除"
markAllAsRead: "全部標示為已讀" markAllAsRead: "全部標示為已讀"
goBack: "返回" goBack: "返回"
@ -1696,9 +1697,10 @@ _2fa:
step1: "首先,在您的裝置上安裝驗證程式,例如 {a} 或 {b}。" step1: "首先,在您的裝置上安裝驗證程式,例如 {a} 或 {b}。"
step2: "然後,掃描螢幕上的 QR 碼。" step2: "然後,掃描螢幕上的 QR 碼。"
step2Click: "您可以點擊 QR 碼,以使用裝置上的驗證應用程式或金鑰環註冊。" step2Click: "您可以點擊 QR 碼,以使用裝置上的驗證應用程式或金鑰環註冊。"
step2Url: "請在桌面版應用程式中輸入以下的 URL" step2Uri: "使用桌面版應用程式時,請輸入以下的 URI"
step3Title: "輸入驗證碼" step3Title: "輸入驗證碼"
step3: "輸入應用程式所提供的權杖以完成設定。" step3: "輸入應用程式所提供的權杖以完成設定。"
setupCompleted: "設定完成"
step4: "從現在開始,任何登入操作都將要求您提供權杖。" step4: "從現在開始,任何登入操作都將要求您提供權杖。"
securityKeyNotSupported: "您的瀏覽器不支援安全金鑰。" securityKeyNotSupported: "您的瀏覽器不支援安全金鑰。"
registerTOTPBeforeKey: "如要註冊安全金鑰或 Passkey請先設定驗證應用程式。" registerTOTPBeforeKey: "如要註冊安全金鑰或 Passkey請先設定驗證應用程式。"
@ -1714,6 +1716,11 @@ _2fa:
renewTOTPConfirm: "目前驗證應用程式的驗證碼將無法使用。" renewTOTPConfirm: "目前驗證應用程式的驗證碼將無法使用。"
renewTOTPOk: "重設" renewTOTPOk: "重設"
renewTOTPCancel: "現在不要" renewTOTPCancel: "現在不要"
checkBackupCodesBeforeCloseThisWizard: "請先確認下列備用驗證碼,再關閉此精靈視窗。"
backupCodes: "備用驗證碼"
backupCodesDescription: "如果驗證應用程式不能用了,可以使用以下的備用驗證碼存取您的帳戶。請務必妥善保管這個驗證碼。每個驗證碼只能使用一次。"
backupCodeUsedWarning: "已使用備用驗證碼。如果無法使用驗證應用程式,請盡快重新設定。"
backupCodesExhaustedWarning: "已使用所有備用驗證碼。如果無法使用驗證應用程式,則將無法再存取您的帳戶。請重新設定您的驗證應用程式。"
_permissions: _permissions:
"read:account": "查看我的帳戶資訊" "read:account": "查看我的帳戶資訊"
"write:account": "更改我的帳戶資訊" "write:account": "更改我的帳戶資訊"
@ -1748,9 +1755,9 @@ _permissions:
"read:gallery-likes": "讀取喜歡的圖片" "read:gallery-likes": "讀取喜歡的圖片"
"write:gallery-likes": "操作喜歡的圖片" "write:gallery-likes": "操作喜歡的圖片"
"read:flash": "檢視 Play" "read:flash": "檢視 Play"
"write:flash": "操作 Play" "write:flash": "編輯 Play"
"read:flash-likes": "檢視 Play 的讚" "read:flash-likes": "檢視 Play 的讚"
"write:flash-likes": "對 Play 的讚進行操作" "write:flash-likes": "編輯 Play 的讚"
_auth: _auth:
shareAccessTitle: "應用程式的存取權限" shareAccessTitle: "應用程式的存取權限"
shareAccess: "要授權「“{name}”」存取您的帳戶嗎?" shareAccess: "要授權「“{name}”」存取您的帳戶嗎?"
@ -2020,7 +2027,9 @@ _deck:
introduction: "組合多個欄位,製作屬於自己的介面吧!" introduction: "組合多個欄位,製作屬於自己的介面吧!"
introduction2: "您可以隨時按畫面右方的「+」新增欄位。" introduction2: "您可以隨時按畫面右方的「+」新增欄位。"
widgetsIntroduction: "請從欄位選單中選擇「編輯小工具」新增小工具。" widgetsIntroduction: "請從欄位選單中選擇「編輯小工具」新增小工具。"
useSimpleUiForNonRootPages: "用簡易 UI 顯示非根頁面" useSimpleUiForNonRootPages: "用簡易介面顯示非根頁面"
usedAsMinWidthWhenFlexible: "如果啟用「自動調整寬度」,此為最小寬度"
flexible: "自動調整寬度"
_columns: _columns:
main: "主列" main: "主列"
widgets: "小工具" widgets: "小工具"

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2023.9.0-beta.2", "version": "2023.9.0-beta.5",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@8.6.10", "packageManager": "pnpm@8.7.4",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",
@ -15,7 +15,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build-pre": "node ./scripts/build-pre.js", "build-pre": "node ./scripts/build-pre.js",
"build": "pnpm build-pre && pnpm -r build && pnpm gulp", "build-assets": "node ./scripts/build-assets.mjs",
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
"build-storybook": "pnpm --filter frontend build-storybook", "build-storybook": "pnpm --filter frontend build-storybook",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
@ -23,7 +24,6 @@
"migrate": "cd packages/backend && pnpm migrate", "migrate": "cd packages/backend && pnpm migrate",
"check:connect": "cd packages/backend && pnpm check:connect", "check:connect": "cd packages/backend && pnpm check:connect",
"migrateandstart": "pnpm migrate && pnpm start", "migrateandstart": "pnpm migrate && pnpm start",
"gulp": "pnpm exec gulp build",
"watch": "pnpm dev", "watch": "pnpm dev",
"dev": "node ./scripts/dev.mjs", "dev": "node ./scripts/dev.mjs",
"lint": "pnpm -r lint", "lint": "pnpm -r lint",
@ -34,7 +34,6 @@
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm -r test", "test": "pnpm -r test",
"test-and-coverage": "pnpm -r test-and-coverage", "test-and-coverage": "pnpm -r test-and-coverage",
"format": "pnpm exec gulp format",
"clean": "node ./scripts/clean.js", "clean": "node ./scripts/clean.js",
"clean-all": "node ./scripts/clean-all.js", "clean-all": "node ./scripts/clean-all.js",
"cleanall": "pnpm clean-all" "cleanall": "pnpm clean-all"
@ -44,23 +43,19 @@
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"dependencies": { "dependencies": {
"execa": "7.2.0", "execa": "8.0.1",
"gulp": "4.0.2", "cssnano": "6.0.1",
"gulp-cssnano": "2.1.3",
"gulp-rename": "2.0.0",
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"typescript": "5.1.6" "postcss": "8.4.27",
"terser": "5.19.2",
"typescript": "5.2.2"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.13", "@typescript-eslint/eslint-plugin": "6.6.0",
"@types/gulp-rename": "2.0.2", "@typescript-eslint/parser": "6.6.0",
"@typescript-eslint/eslint-plugin": "6.2.0",
"@typescript-eslint/parser": "6.2.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.17.2", "cypress": "13.1.0",
"eslint": "8.46.0", "eslint": "8.48.0",
"start-server-and-test": "2.0.0" "start-server-and-test": "2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class PasskeySupport1691959191872 {
name = 'PasskeySupport1691959191872'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_security_key" ADD "counter" bigint NOT NULL DEFAULT '0'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."counter" IS 'The number of times the UserSecurityKey was validated.'`);
await queryRunner.query(`ALTER TABLE "user_security_key" ADD "credentialDeviceType" character varying(32)`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialDeviceType" IS 'The type of Backup Eligibility in authenticator data'`);
await queryRunner.query(`ALTER TABLE "user_security_key" ADD "credentialBackedUp" boolean`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialBackedUp" IS 'Whether or not the credential has been backed up'`);
await queryRunner.query(`ALTER TABLE "user_security_key" ADD "transports" character varying(32) array`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."transports" IS 'The type of the credential returned by the browser'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'The public key of the UserSecurityKey, hex-encoded.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'Timestamp of the last time the UserSecurityKey was used.'`);
await queryRunner.query(`ALTER TABLE "user_security_key" ALTER COLUMN "lastUsed" SET DEFAULT now()`);
await queryRunner.query(`UPDATE "user_security_key" SET "id" = REPLACE(REPLACE(REPLACE(REPLACE(ENCODE(DECODE("id", 'hex'), 'base64'), E'\\n', ''), '+', '-'), '/', '_'), '=', ''), "publicKey" = REPLACE(REPLACE(REPLACE(REPLACE(ENCODE(DECODE("publicKey", 'hex'), 'base64'), E'\\n', ''), '+', '-'), '/', '_'), '=', '')`);
await queryRunner.query(`ALTER TABLE "attestation_challenge" DROP CONSTRAINT "FK_f1a461a618fa1755692d0e0d592"`);
await queryRunner.query(`DROP INDEX "IDX_47efb914aed1f72dd39a306c7b"`);
await queryRunner.query(`DROP INDEX "IDX_f1a461a618fa1755692d0e0d59"`);
await queryRunner.query(`DROP TABLE "attestation_challenge"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TABLE "attestation_challenge" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "challenge" character varying(64) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "registrationChallenge" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_d0ba6786e093f1bcb497572a6b5" PRIMARY KEY ("id", "userId"))`);
await queryRunner.query(`CREATE INDEX "IDX_f1a461a618fa1755692d0e0d59" ON "attestation_challenge" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_47efb914aed1f72dd39a306c7b" ON "attestation_challenge" ("challenge") `);
await queryRunner.query(`ALTER TABLE "attestation_challenge" ADD CONSTRAINT "FK_f1a461a618fa1755692d0e0d592" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS 'Hex-encoded sha256 hash of the challenge.'`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS 'The date challenge was created for expiry purposes.'`);
await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.'`);
await queryRunner.query(`UPDATE "user_security_key" SET "id" = ENCODE(DECODE(REPLACE(REPLACE("id" || CASE WHEN LENGTH("id") % 4 = 2 THEN '==' WHEN LENGTH("id") % 4 = 3 THEN '=' ELSE '' END, '-', '+'), '_', '/'), 'base64'), 'hex'), "publicKey" = ENCODE(DECODE(REPLACE(REPLACE("publicKey" || CASE WHEN LENGTH("publicKey") % 4 = 2 THEN '==' WHEN LENGTH("publicKey") % 4 = 3 THEN '=' ELSE '' END, '-', '+'), '_', '/'), 'base64'), 'hex')`);
await queryRunner.query(`ALTER TABLE "user_security_key" ALTER COLUMN "lastUsed" DROP DEFAULT`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'The date of the last time the UserSecurityKey was successfully validated.'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'Variable-length public key used to verify attestations (hex-encoded).'`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."transports" IS 'The type of the credential returned by the browser'`);
await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "transports"`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialBackedUp" IS 'Whether or not the credential has been backed up'`);
await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "credentialBackedUp"`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialDeviceType" IS 'The type of Backup Eligibility in authenticator data'`);
await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "credentialDeviceType"`);
await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."counter" IS 'The number of times the UserSecurityKey was validated.'`);
await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "counter"`);
}
}

View File

@ -56,36 +56,37 @@
"utf-8-validate": "^6.0.3" "utf-8-validate": "^6.0.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.367.0", "@aws-sdk/client-s3": "3.400.0",
"@aws-sdk/lib-storage": "3.367.0", "@aws-sdk/lib-storage": "3.400.0",
"@aws-sdk/node-http-handler": "3.360.0", "@aws-sdk/node-http-handler": "3.374.0",
"@bull-board/api": "5.7.1", "@bull-board/api": "5.8.1",
"@bull-board/fastify": "5.7.1", "@bull-board/fastify": "5.8.1",
"@bull-board/ui": "5.7.1", "@bull-board/ui": "5.8.1",
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0", "@fastify/accepts": "4.2.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "9.0.4",
"@fastify/cors": "8.3.0", "@fastify/cors": "8.3.0",
"@fastify/express": "2.3.0", "@fastify/express": "2.3.0",
"@fastify/http-proxy": "9.2.1", "@fastify/http-proxy": "9.2.1",
"@fastify/multipart": "7.7.3", "@fastify/multipart": "7.7.3",
"@fastify/static": "6.10.2", "@fastify/static": "6.11.0",
"@fastify/view": "8.0.0", "@fastify/view": "8.0.0",
"@nestjs/common": "10.1.2", "@nestjs/common": "10.2.4",
"@nestjs/core": "10.1.2", "@nestjs/core": "10.2.4",
"@nestjs/testing": "10.1.2", "@nestjs/testing": "10.2.4",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0", "@simplewebauthn/server": "8.1.1",
"@sinonjs/fake-timers": "11.1.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.72", "@swc/core": "1.3.82",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "6.0.1",
"async-mutex": "^0.4.0", "async-mutex": "0.4.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "4.6.3", "bullmq": "4.8.0",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.1", "cbor": "9.0.1",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -96,7 +97,7 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.21.0", "fastify": "4.22.2",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.5.0", "file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
@ -112,14 +113,15 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "22.1.0", "jsdom": "22.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.2.0", "jsonld": "8.2.1",
"jsrsasign": "10.8.6", "jsrsasign": "10.8.6",
"meilisearch": "0.33.0", "meilisearch": "0.34.1",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"microformats-parser": "1.4.1", "microformats-parser": "1.4.1",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nanoid": "4.0.2",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.4", "nodemailer": "6.9.4",
@ -130,7 +132,7 @@
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.1.4", "otpauth": "9.1.4",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.1", "pg": "8.11.3",
"pkce-challenge": "4.0.1", "pkce-challenge": "4.0.1",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
@ -140,84 +142,85 @@
"qrcode": "1.5.3", "qrcode": "1.5.3",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.20.1", "re2": "1.20.3",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rename": "1.0.4", "rename": "1.0.4",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"sharp": "0.32.4", "sharp": "0.32.5",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.18.9", "systeminformation": "5.21.4",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.7", "tsc-alias": "1.8.7",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.17", "typeorm": "0.3.17",
"typescript": "5.1.6", "typescript": "5.2.2",
"ulid": "2.3.0", "ulid": "2.3.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.4", "web-push": "3.6.5",
"ws": "8.13.0", "ws": "8.13.0",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.6.2", "@jest/globals": "29.6.4",
"@swc/jest": "0.2.27", "@simplewebauthn/typescript-types": "8.0.0",
"@swc/jest": "0.2.29",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.2", "@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.3",
"@types/body-parser": "1.19.2", "@types/body-parser": "1.19.2",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/color-convert": "2.0.0", "@types/color-convert": "2.0.1",
"@types/content-disposition": "0.5.5", "@types/content-disposition": "0.5.6",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/http-link-header": "1.0.3", "@types/http-link-header": "1.0.3",
"@types/jest": "29.5.3", "@types/jest": "29.5.4",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1", "@types/jsdom": "21.1.2",
"@types/jsonld": "1.5.9", "@types/jsonld": "1.5.9",
"@types/jsrsasign": "10.5.8", "@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/ms": "0.7.31", "@types/ms": "0.7.31",
"@types/node": "20.4.5", "@types/node": "20.5.9",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.9", "@types/nodemailer": "6.4.9",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.2",
"@types/oauth2orize": "1.11.0", "@types/oauth2orize": "1.11.1",
"@types/oauth2orize-pkce": "0.1.0", "@types/oauth2orize-pkce": "0.1.0",
"@types/pg": "8.10.2", "@types/pg": "8.10.2",
"@types/pug": "2.0.6", "@types/pug": "2.0.6",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/qrcode": "1.5.1", "@types/qrcode": "1.5.2",
"@types/random-seed": "0.3.3", "@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.4",
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.9.0",
"@types/semver": "7.5.0", "@types/semver": "7.5.1",
"@types/sharp": "0.32.0", "@types/sharp": "0.32.0",
"@types/simple-oauth2": "5.0.4", "@types/simple-oauth2": "5.0.4",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/vary": "1.1.0", "@types/vary": "1.1.0",
"@types/web-push": "3.3.2", "@types/web-push": "3.6.0",
"@types/ws": "8.5.5", "@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.2.0", "@typescript-eslint/eslint-plugin": "6.6.0",
"@typescript-eslint/parser": "6.2.0", "@typescript-eslint/parser": "6.6.0",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.46.0", "eslint": "8.48.0",
"eslint-plugin-import": "2.28.0", "eslint-plugin-import": "2.28.1",
"execa": "7.2.0", "execa": "8.0.1",
"jest": "29.6.2", "jest": "29.6.4",
"jest-mock": "29.6.2", "jest-mock": "29.6.3",
"simple-oauth2": "5.0.0" "simple-oauth2": "5.0.0"
} }
} }

View File

@ -8,7 +8,6 @@ import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; import { QueueProcessorService } from '@/queue/QueueProcessorService.js';
import { NestLogger } from '@/NestLogger.js'; import { NestLogger } from '@/NestLogger.js';
import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
import { JanitorService } from '@/daemons/JanitorService.js';
import { QueueStatsService } from '@/daemons/QueueStatsService.js'; import { QueueStatsService } from '@/daemons/QueueStatsService.js';
import { ServerStatsService } from '@/daemons/ServerStatsService.js'; import { ServerStatsService } from '@/daemons/ServerStatsService.js';
import { ServerService } from '@/server/ServerService.js'; import { ServerService } from '@/server/ServerService.js';
@ -25,7 +24,6 @@ export async function server() {
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
app.get(ChartManagementService).start(); app.get(ChartManagementService).start();
app.get(JanitorService).start();
app.get(QueueStatsService).start(); app.get(QueueStatsService).start();
app.get(ServerStatsService).start(); app.get(ServerStatsService).start();
} }

View File

@ -3,10 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/**
* Config loader
*/
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path'; import { dirname, resolve } from 'node:path';
@ -23,11 +19,9 @@ type RedisOptionsSource = Partial<RedisOptions> & {
}; };
/** /**
* *
*/ */
export type Source = { type Source = {
repository_url?: string;
feedback_url?: string;
url: string; url: string;
port?: number; port?: number;
socket?: string; socket?: string;
@ -70,8 +64,6 @@ export type Source = {
maxFileSize?: number; maxFileSize?: number;
accesslog?: string;
clusterLimit?: number; clusterLimit?: number;
id: string; id: string;
@ -93,12 +85,63 @@ export type Source = {
videoThumbnailGenerator?: string; videoThumbnailGenerator?: string;
signToActivityPubGet?: boolean; signToActivityPubGet?: boolean;
perChannelMaxNoteCacheCount?: number;
perUserNotificationsMaxCount?: number;
deactivateAntennaThreshold?: number;
}; };
/** export type Config = {
* Misskeyが自動的に() url: string;
*/ port: number;
export type Mixin = { socket: string | undefined;
chmodSocket: string | undefined;
disableHsts: boolean | undefined;
db: {
host: string;
port: number;
db: string;
user: string;
pass: string;
disableCache?: boolean;
extra?: { [x: string]: string };
};
dbReplications: boolean | undefined;
dbSlaves: {
host: string;
port: number;
db: string;
user: string;
pass: string;
}[] | undefined;
meilisearch: {
host: string;
port: string;
apiKey: string;
ssl?: boolean;
index: string;
scope?: 'local' | 'global' | string[];
} | undefined;
proxy: string | undefined;
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
allowedPrivateNetworks: string[] | undefined;
maxFileSize: number | undefined;
clusterLimit: number | undefined;
id: string;
outgoingAddress: string | undefined;
outgoingAddressFamily: 'ipv4' | 'ipv6' | 'dual' | undefined;
deliverJobConcurrency: number | undefined;
inboxJobConcurrency: number | undefined;
relashionshipJobConcurrency: number | undefined;
deliverJobPerSec: number | undefined;
inboxJobPerSec: number | undefined;
relashionshipJobPerSec: number | undefined;
deliverJobMaxAttempts: number | undefined;
inboxJobMaxAttempts: number | undefined;
proxyRemoteFiles: boolean | undefined;
signToActivityPubGet: boolean | undefined;
version: string; version: string;
host: string; host: string;
hostname: string; hostname: string;
@ -117,10 +160,11 @@ export type Mixin = {
redis: RedisOptions & RedisOptionsSource; redis: RedisOptions & RedisOptionsSource;
redisForPubsub: RedisOptions & RedisOptionsSource; redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource; redisForJobQueue: RedisOptions & RedisOptionsSource;
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
}; };
export type Config = Source & Mixin;
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
@ -138,7 +182,7 @@ const path = process.env.MISSKEY_CONFIG_YML
? resolve(dir, 'test.yml') ? resolve(dir, 'test.yml')
: resolve(dir, 'default.yml'); : resolve(dir, 'default.yml');
export function loadConfig() { export function loadConfig(): Config {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
const clientManifest = clientManifestExists ? const clientManifest = clientManifestExists ?
@ -146,43 +190,72 @@ export function loadConfig() {
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const mixin = {} as Mixin;
const url = tryCreateUrl(config.url); const url = tryCreateUrl(config.url);
const version = meta.version;
config.url = url.origin; const host = url.host;
const hostname = url.hostname;
config.port = config.port ?? parseInt(process.env.PORT ?? '', 10); const scheme = url.protocol.replace(/:$/, '');
const wsScheme = scheme.replace('http', 'ws');
mixin.version = meta.version;
mixin.host = url.host;
mixin.hostname = url.hostname;
mixin.scheme = url.protocol.replace(/:$/, '');
mixin.wsScheme = mixin.scheme.replace('http', 'ws');
mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`;
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/_boot_.ts'];
mixin.clientManifestExists = clientManifestExists;
const externalMediaProxy = config.mediaProxy ? const externalMediaProxy = config.mediaProxy ?
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
: null; : null;
const internalMediaProxy = `${mixin.scheme}://${mixin.host}/proxy`; const internalMediaProxy = `${scheme}://${host}/proxy`;
mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy; const redis = convertRedisOptions(config.redis, host);
mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy;
mixin.videoThumbnailGenerator = config.videoThumbnailGenerator ? return {
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator version,
: null; url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
mixin.redis = convertRedisOptions(config.redis, mixin.host); socket: config.socket,
mixin.redisForPubsub = config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, mixin.host) : mixin.redis; chmodSocket: config.chmodSocket,
mixin.redisForJobQueue = config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, mixin.host) : mixin.redis; disableHsts: config.disableHsts,
host,
return Object.assign(config, mixin); hostname,
scheme,
wsScheme,
wsUrl: `${wsScheme}://${host}`,
apiUrl: `${scheme}://${host}/api`,
authUrl: `${scheme}://${host}/auth`,
driveUrl: `${scheme}://${host}/files`,
db: config.db,
dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves,
meilisearch: config.meilisearch,
redis,
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
id: config.id,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
proxyBypassHosts: config.proxyBypassHosts,
allowedPrivateNetworks: config.allowedPrivateNetworks,
maxFileSize: config.maxFileSize,
clusterLimit: config.clusterLimit,
outgoingAddress: config.outgoingAddress,
outgoingAddressFamily: config.outgoingAddressFamily,
deliverJobConcurrency: config.deliverJobConcurrency,
inboxJobConcurrency: config.inboxJobConcurrency,
relashionshipJobConcurrency: config.relashionshipJobConcurrency,
deliverJobPerSec: config.deliverJobPerSec,
inboxJobPerSec: config.inboxJobPerSec,
relashionshipJobPerSec: config.relashionshipJobPerSec,
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles,
signToActivityPubGet: config.signToActivityPubGet,
mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ?
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
: null,
userAgent: `Misskey/${version} (${config.url})`,
clientEntry: clientManifest['src/_boot_.ts'],
clientManifestExists: clientManifestExists,
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
};
} }
function tryCreateUrl(url: string) { function tryCreateUrl(url: string) {

View File

@ -43,7 +43,7 @@ import { RelayService } from './RelayService.js';
import { RoleService } from './RoleService.js'; import { RoleService } from './RoleService.js';
import { S3Service } from './S3Service.js'; import { S3Service } from './S3Service.js';
import { SignupService } from './SignupService.js'; import { SignupService } from './SignupService.js';
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; import { WebAuthnService } from './WebAuthnService.js';
import { UserBlockingService } from './UserBlockingService.js'; import { UserBlockingService } from './UserBlockingService.js';
import { CacheService } from './CacheService.js'; import { CacheService } from './CacheService.js';
import { UserFollowingService } from './UserFollowingService.js'; import { UserFollowingService } from './UserFollowingService.js';
@ -168,7 +168,7 @@ const $RelayService: Provider = { provide: 'RelayService', useExisting: RelaySer
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService }; const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service }; const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService }; const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService }; const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService }; const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
@ -296,7 +296,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleService, RoleService,
S3Service, S3Service,
SignupService, SignupService,
TwoFactorAuthenticationService, WebAuthnService,
UserBlockingService, UserBlockingService,
CacheService, CacheService,
UserFollowingService, UserFollowingService,
@ -417,7 +417,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleService, $RoleService,
$S3Service, $S3Service,
$SignupService, $SignupService,
$TwoFactorAuthenticationService, $WebAuthnService,
$UserBlockingService, $UserBlockingService,
$CacheService, $CacheService,
$UserFollowingService, $UserFollowingService,
@ -539,7 +539,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleService, RoleService,
S3Service, S3Service,
SignupService, SignupService,
TwoFactorAuthenticationService, WebAuthnService,
UserBlockingService, UserBlockingService,
CacheService, CacheService,
UserFollowingService, UserFollowingService,
@ -659,7 +659,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleService, $RoleService,
$S3Service, $S3Service,
$SignupService, $SignupService,
$TwoFactorAuthenticationService, $WebAuthnService,
$UserBlockingService, $UserBlockingService,
$CacheService, $CacheService,
$UserFollowingService, $UserFollowingService,

View File

@ -8,6 +8,7 @@ import { ulid } from 'ulid';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { genAid, parseAid } from '@/misc/id/aid.js'; import { genAid, parseAid } from '@/misc/id/aid.js';
import { genAidx, parseAidx } from '@/misc/id/aidx.js';
import { genMeid, parseMeid } from '@/misc/id/meid.js'; import { genMeid, parseMeid } from '@/misc/id/meid.js';
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
import { genObjectId, parseObjectId } from '@/misc/id/object-id.js'; import { genObjectId, parseObjectId } from '@/misc/id/object-id.js';
@ -31,6 +32,7 @@ export class IdService {
switch (this.method) { switch (this.method) {
case 'aid': return genAid(date); case 'aid': return genAid(date);
case 'aidx': return genAidx(date);
case 'meid': return genMeid(date); case 'meid': return genMeid(date);
case 'meidg': return genMeidg(date); case 'meidg': return genMeidg(date);
case 'ulid': return ulid(date.getTime()); case 'ulid': return ulid(date.getTime());
@ -43,6 +45,7 @@ export class IdService {
public parse(id: string): { date: Date; } { public parse(id: string): { date: Date; } {
switch (this.method) { switch (this.method) {
case 'aid': return parseAid(id); case 'aid': return parseAid(id);
case 'aidx': return parseAidx(id);
case 'objectid': return parseObjectId(id); case 'objectid': return parseObjectId(id);
case 'meid': return parseMeid(id); case 'meid': return parseMeid(id);
case 'meidg': return parseMeidg(id); case 'meidg': return parseMeidg(id);

View File

@ -16,7 +16,7 @@ import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class MetaService implements OnApplicationShutdown { export class MetaService implements OnApplicationShutdown {
private cache: MiMeta | undefined; private cache: MiMeta | undefined;
private intervalId: NodeJS.Timer; private intervalId: NodeJS.Timeout;
constructor( constructor(
@Inject(DI.redisForSub) @Inject(DI.redisForSub)

View File

@ -334,7 +334,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel) { if (data.channel) {
this.redisClient.xadd( this.redisClient.xadd(
`channelTimeline:${data.channel.id}`, `channelTimeline:${data.channel.id}`,
'MAXLEN', '~', '1000', 'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
'*', '*',
'note', note.id); 'note', note.id);
} }

View File

@ -17,12 +17,16 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
@Injectable() @Injectable()
export class NotificationService implements OnApplicationShutdown { export class NotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController(); #shutdownController = new AbortController();
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
@ -96,7 +100,7 @@ export class NotificationService implements OnApplicationShutdown {
const redisIdPromise = this.redisClient.xadd( const redisIdPromise = this.redisClient.xadd(
`notificationTimeline:${notifieeId}`, `notificationTimeline:${notifieeId}`,
'MAXLEN', '~', '300', 'MAXLEN', '~', this.config.perUserNotificationsMaxCount.toString(),
'*', '*',
'data', JSON.stringify(notification)); 'data', JSON.stringify(notification));

View File

@ -1,446 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as crypto from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import * as jsrsasign from 'jsrsasign';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
const ECC_PRELUDE = Buffer.from([0x04]);
const NULL_BYTE = Buffer.from([0]);
const PEM_PRELUDE = Buffer.from(
'3059301306072a8648ce3d020106082a8648ce3d030107034200',
'hex',
);
// Android Safetynet attestations are signed with this cert:
const GSR2 = `-----BEGIN CERTIFICATE-----
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
-----END CERTIFICATE-----\n`;
function base64URLDecode(source: string) {
return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64');
}
function getCertSubject(certificate: string) {
const subjectCert = new jsrsasign.X509();
subjectCert.readCertPEM(certificate);
const subjectString = subjectCert.getSubjectString();
const subjectFields = subjectString.slice(1).split('/');
const fields = {} as Record<string, string>;
for (const field of subjectFields) {
const eqIndex = field.indexOf('=');
fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1);
}
return fields;
}
function verifyCertificateChain(certificates: string[]) {
let valid = true;
for (let i = 0; i < certificates.length; i++) {
const Cert = certificates[i];
const certificate = new jsrsasign.X509();
certificate.readCertPEM(Cert);
const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1];
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
if (certStruct == null) throw new Error('certStruct is null');
const algorithm = certificate.getSignatureAlgorithmField();
const signatureHex = certificate.getSignatureValueHex();
// Verify against CA
const Signature = new jsrsasign.KJUR.crypto.Signature({ alg: algorithm });
Signature.init(CACert);
Signature.updateHex(certStruct);
valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate
}
return valid;
}
function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') {
if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) {
pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91);
type = 'PUBLIC KEY';
}
const cert = pemBuffer.toString('base64');
const keyParts = [];
const max = Math.ceil(cert.length / 64);
let start = 0;
for (let i = 0; i < max; i++) {
keyParts.push(cert.substring(start, start + 64));
start += 64;
}
return (
`-----BEGIN ${type}-----\n` +
keyParts.join('\n') +
`\n-----END ${type}-----\n`
);
}
@Injectable()
export class TwoFactorAuthenticationService {
constructor(
@Inject(DI.config)
private config: Config,
) {
}
@bindThis
public hash(data: Buffer) {
return crypto
.createHash('sha256')
.update(data)
.digest();
}
@bindThis
public verifySignin({
publicKey,
authenticatorData,
clientDataJSON,
clientData,
signature,
challenge,
}: {
publicKey: Buffer,
authenticatorData: Buffer,
clientDataJSON: Buffer,
clientData: any,
signature: Buffer,
challenge: string
}) {
if (clientData.type !== 'webauthn.get') {
throw new Error('type is not webauthn.get');
}
if (this.hash(clientData.challenge).toString('hex') !== challenge) {
throw new Error('challenge mismatch');
}
if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
throw new Error('origin mismatch');
}
const verificationData = Buffer.concat(
[authenticatorData, this.hash(clientDataJSON)],
32 + authenticatorData.length,
);
return crypto
.createVerify('SHA256')
.update(verificationData)
.verify(PEMString(publicKey), signature);
}
@bindThis
public getProcedures() {
return {
none: {
verify({ publicKey }: { publicKey: Map<number, Buffer> }) {
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32,
);
return {
publicKey: publicKeyU2F,
valid: true,
};
},
},
'android-key': {
verify({
attStmt,
authenticatorData,
clientDataHash,
publicKey,
rpIdHash,
credentialId,
}: {
attStmt: any,
authenticatorData: Buffer,
clientDataHash: Buffer,
publicKey: Map<number, any>;
rpIdHash: Buffer,
credentialId: Buffer,
}) {
if (attStmt.alg !== -7) {
throw new Error('alg mismatch');
}
const verificationData = Buffer.concat([
authenticatorData,
clientDataHash,
]);
const attCert: Buffer = attStmt.x5c[0];
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32,
);
if (!attCert.equals(publicKeyData)) {
throw new Error('public key mismatch');
}
const isValid = crypto
.createVerify('SHA256')
.update(verificationData)
.verify(PEMString(attCert), attStmt.sig);
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
return {
valid: isValid,
publicKey: publicKeyData,
};
},
},
// what a stupid attestation
'android-safetynet': {
verify: ({
attStmt,
authenticatorData,
clientDataHash,
publicKey,
rpIdHash,
credentialId,
}: {
attStmt: any,
authenticatorData: Buffer,
clientDataHash: Buffer,
publicKey: Map<number, any>;
rpIdHash: Buffer,
credentialId: Buffer,
}) => {
const verificationData = this.hash(
Buffer.concat([authenticatorData, clientDataHash]),
);
const jwsParts = attStmt.response.toString('utf-8').split('.');
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
const response = JSON.parse(
base64URLDecode(jwsParts[1]).toString('utf-8'),
);
const signature = jwsParts[2];
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
throw new Error('invalid nonce');
}
const certificateChain = header.x5c
.map((key: any) => PEMString(key))
.concat([GSR2]);
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
throw new Error('invalid common name');
}
if (!verifyCertificateChain(certificateChain)) {
throw new Error('Invalid certificate chain!');
}
const signatureBase = Buffer.from(
jwsParts[0] + '.' + jwsParts[1],
'utf-8',
);
const valid = crypto
.createVerify('sha256')
.update(signatureBase)
.verify(certificateChain[0], base64URLDecode(signature));
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32,
);
return {
valid,
publicKey: publicKeyData,
};
},
},
packed: {
verify({
attStmt,
authenticatorData,
clientDataHash,
publicKey,
rpIdHash,
credentialId,
}: {
attStmt: any,
authenticatorData: Buffer,
clientDataHash: Buffer,
publicKey: Map<number, any>;
rpIdHash: Buffer,
credentialId: Buffer,
}) {
const verificationData = Buffer.concat([
authenticatorData,
clientDataHash,
]);
if (attStmt.x5c) {
const attCert = attStmt.x5c[0];
const validSignature = crypto
.createVerify('SHA256')
.update(verificationData)
.verify(PEMString(attCert), attStmt.sig);
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32,
);
return {
valid: validSignature,
publicKey: publicKeyData,
};
} else if (attStmt.ecdaaKeyId) {
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
throw new Error('ECDAA-Verify is not supported');
} else {
if (attStmt.alg !== -7) throw new Error('alg mismatch');
throw new Error('self attestation is not supported');
}
},
},
'fido-u2f': {
verify({
attStmt,
authenticatorData,
clientDataHash,
publicKey,
rpIdHash,
credentialId,
}: {
attStmt: any,
authenticatorData: Buffer,
clientDataHash: Buffer,
publicKey: Map<number, any>,
rpIdHash: Buffer,
credentialId: Buffer
}) {
const x5c: Buffer[] = attStmt.x5c;
if (x5c.length !== 1) {
throw new Error('x5c length does not match expectation');
}
const attCert = x5c[0];
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
const negTwo: Buffer = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree: Buffer = publicKey.get(-3);
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32,
);
const verificationData = Buffer.concat([
NULL_BYTE,
rpIdHash,
clientDataHash,
credentialId,
publicKeyU2F,
]);
const validSignature = crypto
.createVerify('SHA256')
.update(verificationData)
.verify(PEMString(attCert), attStmt.sig);
return {
valid: validSignature,
publicKey: publicKeyU2F,
};
},
},
};
}
}

View File

@ -0,0 +1,252 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import {
generateAuthenticationOptions,
generateRegistrationOptions, verifyAuthenticationResponse,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers';
import { DI } from '@/di-symbols.js';
import type { UserSecurityKeysRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { MiUser } from '@/models/index.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type {
AuthenticationResponseJSON,
AuthenticatorTransportFuture,
CredentialDeviceType,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialDescriptorFuture,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/typescript-types';
@Injectable()
export class WebAuthnService {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.config)
private config: Config,
@Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository,
private metaService: MetaService,
) {
}
@bindThis
public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> {
const instance = await this.metaService.fetch();
return {
origin: this.config.url,
rpId: this.config.host,
rpName: instance.name ?? this.config.host,
rpIcon: instance.iconUrl ?? undefined,
};
}
@bindThis
public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
const relyingParty = await this.getRelyingParty();
const keys = await this.userSecurityKeysRepository.findBy({
userId: userId,
});
const registrationOptions = await generateRegistrationOptions({
rpName: relyingParty.rpName,
rpID: relyingParty.rpId,
userID: userId,
userName: userName,
userDisplayName: userDisplayName,
attestationType: 'indirect',
excludeCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
id: Buffer.from(key.id, 'base64url'),
type: 'public-key',
transports: key.transports ?? undefined,
})),
authenticatorSelection: {
residentKey: 'required',
userVerification: 'preferred',
},
});
await this.redisClient.setex(`webauthn:challenge:${userId}`, 90, registrationOptions.challenge);
return registrationOptions;
}
@bindThis
public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{
credentialID: Uint8Array;
credentialPublicKey: Uint8Array;
attestationObject: Uint8Array;
fmt: AttestationFormat;
counter: number;
userVerified: boolean;
credentialDeviceType: CredentialDeviceType;
credentialBackedUp: boolean;
transports?: AuthenticatorTransportFuture[];
}> {
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
if (!challenge) {
throw new IdentifiableError('7dbfb66c-9216-4e2b-9c27-cef2ac8efb84', 'challenge not found');
}
await this.redisClient.del(`webauthn:challenge:${userId}`);
const relyingParty = await this.getRelyingParty();
let verification;
try {
verification = await verifyRegistrationResponse({
response: response,
expectedChallenge: challenge,
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
requireUserVerification: true,
});
} catch (error) {
console.error(error);
throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed');
}
const { verified } = verification;
if (!verified || !verification.registrationInfo) {
throw new IdentifiableError('bb333667-3832-4a80-8bb5-c505be7d710d', 'verification failed');
}
const { registrationInfo } = verification;
return {
credentialID: registrationInfo.credentialID,
credentialPublicKey: registrationInfo.credentialPublicKey,
attestationObject: registrationInfo.attestationObject,
fmt: registrationInfo.fmt,
counter: registrationInfo.counter,
userVerified: registrationInfo.userVerified,
credentialDeviceType: registrationInfo.credentialDeviceType,
credentialBackedUp: registrationInfo.credentialBackedUp,
transports: response.response.transports,
};
}
@bindThis
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
const keys = await this.userSecurityKeysRepository.findBy({
userId: userId,
});
if (keys.length === 0) {
throw new IdentifiableError('f27fd449-9af4-4841-9249-1f989b9fa4a4', 'no keys found');
}
const authenticationOptions = await generateAuthenticationOptions({
allowCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
id: Buffer.from(key.id, 'base64url'),
type: 'public-key',
transports: key.transports ?? undefined,
})),
userVerification: 'preferred',
});
await this.redisClient.setex(`webauthn:challenge:${userId}`, 90, authenticationOptions.challenge);
return authenticationOptions;
}
@bindThis
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
if (!challenge) {
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', 'challenge not found');
}
await this.redisClient.del(`webauthn:challenge:${userId}`);
const key = await this.userSecurityKeysRepository.findOneBy({
id: response.id,
userId: userId,
});
if (!key) {
throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'unknown key');
}
// マイグレーション
if (key.counter === 0 && key.publicKey.length === 87) {
const cert = new Uint8Array(Buffer.from(key.publicKey, 'base64url'));
if (cert[0] === 0x04) { // 前の実装ではいつも 0x04 で始まっていた
const halfLength = (cert.length - 1) / 2;
const cborMap = new Map<number, number | ArrayBufferLike>();
cborMap.set(1, 2); // kty, EC2
cborMap.set(3, -7); // alg, ES256
cborMap.set(-1, 1); // crv, P256
cborMap.set(-2, cert.slice(1, halfLength + 1)); // x
cborMap.set(-3, cert.slice(halfLength + 1)); // y
const cborPubKey = Buffer.from(isoCBOR.encode(cborMap)).toString('base64url');
await this.userSecurityKeysRepository.update({
id: response.id,
userId: userId,
}, {
publicKey: cborPubKey,
});
key.publicKey = cborPubKey;
}
}
const relyingParty = await this.getRelyingParty();
let verification;
try {
verification = await verifyAuthenticationResponse({
response: response,
expectedChallenge: challenge,
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
authenticator: {
credentialID: Buffer.from(key.id, 'base64url'),
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
},
requireUserVerification: true,
});
} catch (error) {
console.error(error);
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed');
}
const { verified, authenticationInfo } = verification;
if (!verified) {
return false;
}
await this.userSecurityKeysRepository.update({
id: response.id,
userId: userId,
}, {
lastUsed: new Date(),
counter: authenticationInfo.newCounter,
credentialDeviceType: authenticationInfo.credentialDeviceType,
credentialBackedUp: authenticationInfo.credentialBackedUp,
});
return verified;
}
}

View File

@ -23,7 +23,7 @@ import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class ChartManagementService implements OnApplicationShutdown { export class ChartManagementService implements OnApplicationShutdown {
private charts; private charts;
private saveIntervalId: NodeJS.Timer; private saveIntervalId: NodeJS.Timeout;
constructor( constructor(
private federationChart: FederationChart, private federationChart: FederationChart,

View File

@ -6,7 +6,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { JanitorService } from './JanitorService.js';
import { QueueStatsService } from './QueueStatsService.js'; import { QueueStatsService } from './QueueStatsService.js';
import { ServerStatsService } from './ServerStatsService.js'; import { ServerStatsService } from './ServerStatsService.js';
@ -16,12 +15,10 @@ import { ServerStatsService } from './ServerStatsService.js';
CoreModule, CoreModule,
], ],
providers: [ providers: [
JanitorService,
QueueStatsService, QueueStatsService,
ServerStatsService, ServerStatsService,
], ],
exports: [ exports: [
JanitorService,
QueueStatsService, QueueStatsService,
ServerStatsService, ServerStatsService,
], ],

View File

@ -1,50 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { LessThan } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { AttestationChallengesRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import type { OnApplicationShutdown } from '@nestjs/common';
const interval = 30 * 60 * 1000;
@Injectable()
export class JanitorService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer;
constructor(
@Inject(DI.attestationChallengesRepository)
private attestationChallengesRepository: AttestationChallengesRepository,
) {
}
/**
* Clean up database occasionally
*/
@bindThis
public start(): void {
const tick = async () => {
await this.attestationChallengesRepository.delete({
createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)),
});
};
tick();
this.intervalId = setInterval(tick, interval);
}
@bindThis
public dispose(): void {
clearInterval(this.intervalId);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View File

@ -19,7 +19,7 @@ const interval = 10000;
@Injectable() @Injectable()
export class QueueStatsService implements OnApplicationShutdown { export class QueueStatsService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer; private intervalId: NodeJS.Timeout;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)

View File

@ -20,7 +20,7 @@ const round = (num: number) => Math.round(num * 10) / 10;
@Injectable() @Injectable()
export class ServerStatsService implements OnApplicationShutdown { export class ServerStatsService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer | null = null; private intervalId: NodeJS.Timeout | null = null;
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,

View File

@ -26,7 +26,6 @@ export const DI = {
userProfilesRepository: Symbol('userProfilesRepository'), userProfilesRepository: Symbol('userProfilesRepository'),
userKeypairsRepository: Symbol('userKeypairsRepository'), userKeypairsRepository: Symbol('userKeypairsRepository'),
userPendingsRepository: Symbol('userPendingsRepository'), userPendingsRepository: Symbol('userPendingsRepository'),
attestationChallengesRepository: Symbol('attestationChallengesRepository'),
userSecurityKeysRepository: Symbol('userSecurityKeysRepository'), userSecurityKeysRepository: Symbol('userSecurityKeysRepository'),
userPublickeysRepository: Symbol('userPublickeysRepository'), userPublickeysRepository: Symbol('userPublickeysRepository'),
userListsRepository: Symbol('userListsRepository'), userListsRepository: Symbol('userListsRepository'),

View File

@ -193,7 +193,7 @@ function nothingToDo<T, V = T>(value: T): V {
export class MemoryKVCache<T, V = T> { export class MemoryKVCache<T, V = T> {
public cache: Map<string, { date: number; value: V; }>; public cache: Map<string, { date: number; value: V; }>;
private lifetime: number; private lifetime: number;
private gcIntervalHandle: NodeJS.Timer; private gcIntervalHandle: NodeJS.Timeout;
private toMapConverter: (value: T) => V; private toMapConverter: (value: T) => V;
private fromMapConverter: (cached: V) => T | undefined; private fromMapConverter: (cached: V) => T | undefined;

View File

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
// AIDX
// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ4の[個体ID] + 長さ4の[カウンタ]
// (c) mei23
// https://misskey.m544.net/notes/71899acdcc9859ec5708ac24
import { customAlphabet } from 'nanoid';
export const aidxRegExp = /^[0-9a-z]{16}$/;
const TIME2000 = 946684800000;
const TIME_LENGTH = 8;
const NODE_LENGTH = 4;
const NOISE_LENGTH = 4;
const nodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', NODE_LENGTH)();
let counter = 0;
function getTime(time: number): string {
time = time - TIME2000;
if (time < 0) time = 0;
return time.toString(36).padStart(TIME_LENGTH, '0').slice(-TIME_LENGTH);
}
function getNoise(): string {
return counter.toString(36).padStart(NOISE_LENGTH, '0').slice(-NOISE_LENGTH);
}
export function genAidx(date: Date): string {
const t = date.getTime();
if (isNaN(t)) throw new Error('Failed to create AIDX: Invalid Date');
counter++;
return getTime(t) + nodeId + getNoise();
}
export function parseAidx(id: string): { date: Date; } {
const time = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000;
return { date: new Date(time) };
}

View File

@ -5,7 +5,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAttestationChallenge, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './index.js'; import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './index.js';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
@ -93,12 +93,6 @@ const $userPendingsRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $attestationChallengesRepository: Provider = {
provide: DI.attestationChallengesRepository,
useFactory: (db: DataSource) => db.getRepository(MiAttestationChallenge),
inject: [DI.db],
};
const $userSecurityKeysRepository: Provider = { const $userSecurityKeysRepository: Provider = {
provide: DI.userSecurityKeysRepository, provide: DI.userSecurityKeysRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey), useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey),
@ -423,7 +417,6 @@ const $userMemosRepository: Provider = {
$userProfilesRepository, $userProfilesRepository,
$userKeypairsRepository, $userKeypairsRepository,
$userPendingsRepository, $userPendingsRepository,
$attestationChallengesRepository,
$userSecurityKeysRepository, $userSecurityKeysRepository,
$userPublickeysRepository, $userPublickeysRepository,
$userListsRepository, $userListsRepository,
@ -491,7 +484,6 @@ const $userMemosRepository: Provider = {
$userProfilesRepository, $userProfilesRepository,
$userKeypairsRepository, $userKeypairsRepository,
$userPendingsRepository, $userPendingsRepository,
$attestationChallengesRepository,
$userSecurityKeysRepository, $userSecurityKeysRepository,
$userPublickeysRepository, $userPublickeysRepository,
$userListsRepository, $userListsRepository,

View File

@ -1,51 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
import { id } from '../id.js';
import { MiUser } from './User.js';
@Entity('attestation_challenge')
export class MiAttestationChallenge {
@PrimaryColumn(id())
public id: string;
@Index()
@PrimaryColumn(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Index()
@Column('varchar', {
length: 64,
comment: 'Hex-encoded sha256 hash of the challenge.',
})
public challenge: string;
@Column('timestamp with time zone', {
comment: 'The date challenge was created for expiry purposes.',
})
public createdAt: Date;
@Column('boolean', {
comment:
'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.',
default: false,
})
public registrationChallenge: boolean;
constructor(data: Partial<MiAttestationChallenge>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -33,6 +33,7 @@ export type MiNotification = {
* followRequestAccepted - * followRequestAccepted -
* achievementEarned - * achievementEarned -
* app - * app -
* test -
*/ */
type: typeof notificationTypes[number]; type: typeof notificationTypes[number];

View File

@ -24,25 +24,48 @@ export class MiUserSecurityKey {
@JoinColumn() @JoinColumn()
public user: MiUser | null; public user: MiUser | null;
@Index()
@Column('varchar', {
comment:
'Variable-length public key used to verify attestations (hex-encoded).',
})
public publicKey: string;
@Column('timestamp with time zone', {
comment:
'The date of the last time the UserSecurityKey was successfully validated.',
})
public lastUsed: Date;
@Column('varchar', { @Column('varchar', {
comment: 'User-defined name for this key', comment: 'User-defined name for this key',
length: 30, length: 30,
}) })
public name: string; public name: string;
@Index()
@Column('varchar', {
comment: 'The public key of the UserSecurityKey, hex-encoded.',
})
public publicKey: string;
@Column('bigint', {
comment: 'The number of times the UserSecurityKey was validated.',
default: 0,
})
public counter: number;
@Column('timestamp with time zone', {
comment: 'Timestamp of the last time the UserSecurityKey was used.',
default: () => 'now()',
})
public lastUsed: Date;
@Column('varchar', {
comment: 'The type of Backup Eligibility in authenticator data',
length: 32, nullable: true,
})
public credentialDeviceType: string | null;
@Column('boolean', {
comment: 'Whether or not the credential has been backed up',
nullable: true,
})
public credentialBackedUp: boolean | null;
@Column('varchar', {
comment: 'The type of the credential returned by the browser',
length: 32, array: true, nullable: true,
})
public transports: string[] | null;
constructor(data: Partial<MiUserSecurityKey>) { constructor(data: Partial<MiUserSecurityKey>) {
if (data == null) return; if (data == null) return;

View File

@ -10,7 +10,6 @@ import { MiAnnouncement } from '@/models/entities/Announcement.js';
import { MiAnnouncementRead } from '@/models/entities/AnnouncementRead.js'; import { MiAnnouncementRead } from '@/models/entities/AnnouncementRead.js';
import { MiAntenna } from '@/models/entities/Antenna.js'; import { MiAntenna } from '@/models/entities/Antenna.js';
import { MiApp } from '@/models/entities/App.js'; import { MiApp } from '@/models/entities/App.js';
import { MiAttestationChallenge } from '@/models/entities/AttestationChallenge.js';
import { MiAuthSession } from '@/models/entities/AuthSession.js'; import { MiAuthSession } from '@/models/entities/AuthSession.js';
import { MiBlocking } from '@/models/entities/Blocking.js'; import { MiBlocking } from '@/models/entities/Blocking.js';
import { MiChannelFollowing } from '@/models/entities/ChannelFollowing.js'; import { MiChannelFollowing } from '@/models/entities/ChannelFollowing.js';
@ -79,7 +78,6 @@ export {
MiAnnouncementRead, MiAnnouncementRead,
MiAntenna, MiAntenna,
MiApp, MiApp,
MiAttestationChallenge,
MiAuthSession, MiAuthSession,
MiBlocking, MiBlocking,
MiChannelFollowing, MiChannelFollowing,
@ -147,7 +145,6 @@ export type AnnouncementsRepository = Repository<MiAnnouncement>;
export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>; export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>;
export type AntennasRepository = Repository<MiAntenna>; export type AntennasRepository = Repository<MiAntenna>;
export type AppsRepository = Repository<MiApp>; export type AppsRepository = Repository<MiApp>;
export type AttestationChallengesRepository = Repository<MiAttestationChallenge>;
export type AuthSessionsRepository = Repository<MiAuthSession>; export type AuthSessionsRepository = Repository<MiAuthSession>;
export type BlockingsRepository = Repository<MiBlocking>; export type BlockingsRepository = Repository<MiBlocking>;
export type ChannelFollowingsRepository = Repository<MiChannelFollowing>; export type ChannelFollowingsRepository = Repository<MiChannelFollowing>;

View File

@ -18,7 +18,6 @@ import { MiAnnouncement } from '@/models/entities/Announcement.js';
import { MiAnnouncementRead } from '@/models/entities/AnnouncementRead.js'; import { MiAnnouncementRead } from '@/models/entities/AnnouncementRead.js';
import { MiAntenna } from '@/models/entities/Antenna.js'; import { MiAntenna } from '@/models/entities/Antenna.js';
import { MiApp } from '@/models/entities/App.js'; import { MiApp } from '@/models/entities/App.js';
import { MiAttestationChallenge } from '@/models/entities/AttestationChallenge.js';
import { MiAuthSession } from '@/models/entities/AuthSession.js'; import { MiAuthSession } from '@/models/entities/AuthSession.js';
import { MiBlocking } from '@/models/entities/Blocking.js'; import { MiBlocking } from '@/models/entities/Blocking.js';
import { MiChannelFollowing } from '@/models/entities/ChannelFollowing.js'; import { MiChannelFollowing } from '@/models/entities/ChannelFollowing.js';
@ -143,7 +142,6 @@ export const entities = [
MiUserNotePining, MiUserNotePining,
MiUserSecurityKey, MiUserSecurityKey,
MiUsedUsername, MiUsedUsername,
MiAttestationChallenge,
MiFollowing, MiFollowing,
MiFollowRequest, MiFollowRequest,
MiMuting, MiMuting,

View File

@ -10,6 +10,7 @@ import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepositor
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Config } from '@/config.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -18,6 +19,9 @@ export class CleanProcessorService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.userIpsRepository) @Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository, private userIpsRepository: UserIpsRepository,
@ -54,12 +58,14 @@ export class CleanProcessorService {
reason: 'word', reason: 'word',
}); });
// 7日以上使われてないアンテナを停止 // 使われてないアンテナを停止
this.antennasRepository.update({ if (this.config.deactivateAntennaThreshold > 0) {
lastUsedAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 7))), this.antennasRepository.update({
}, { lastUsedAt: LessThan(new Date(Date.now() - this.config.deactivateAntennaThreshold)),
isActive: false, }, {
}); isActive: false,
});
}
const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign')
.where('assign.expiresAt IS NOT NULL') .where('assign.expiresAt IS NOT NULL')

View File

@ -35,10 +35,10 @@ export class NodeinfoServerService {
@bindThis @bindThis
public getLinks() { public getLinks() {
return [/* (awaiting release) { return [{
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
href: config.url + nodeinfo2_1path href: this.config.url + nodeinfo2_1path
}, */{ }, {
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
href: this.config.url + nodeinfo2_0path, href: this.config.url + nodeinfo2_0path,
}]; }];
@ -46,7 +46,7 @@ export class NodeinfoServerService {
@bindThis @bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const nodeinfo2 = async () => { const nodeinfo2 = async (version: number) => {
const now = Date.now(); const now = Date.now();
const notesChart = await this.notesChart.getChart('hour', 1, null); const notesChart = await this.notesChart.getChart('hour', 1, null);
@ -73,11 +73,11 @@ export class NodeinfoServerService {
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
return { // eslint-disable-next-line @typescript-eslint/no-explicit-any
const document: any = {
software: { software: {
name: 'misskey', name: 'misskey',
version: this.config.version, version: this.config.version,
repository: meta.repositoryUrl,
}, },
protocols: ['activitypub'], protocols: ['activitypub'],
services: { services: {
@ -114,23 +114,36 @@ export class NodeinfoServerService {
themeColor: meta.themeColor ?? '#86b300', themeColor: meta.themeColor ?? '#86b300',
}, },
}; };
if (version >= 21) {
document.software.repository = meta.repositoryUrl;
document.software.homepage = meta.repositoryUrl;
}
return document;
}; };
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
fastify.get(nodeinfo2_1path, async (request, reply) => { fastify.get(nodeinfo2_1path, async (request, reply) => {
const base = await cache.fetch(() => nodeinfo2()); const base = await cache.fetch(() => nodeinfo2(21));
reply.header('Cache-Control', 'public, max-age=600'); reply
.type(
'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"',
)
.header('Cache-Control', 'public, max-age=600');
return { version: '2.1', ...base }; return { version: '2.1', ...base };
}); });
fastify.get(nodeinfo2_0path, async (request, reply) => { fastify.get(nodeinfo2_0path, async (request, reply) => {
const base = await cache.fetch(() => nodeinfo2()); const base = await cache.fetch(() => nodeinfo2(20));
delete (base as any).software.repository; delete (base as any).software.repository;
reply.header('Cache-Control', 'public, max-age=600'); reply
.type(
'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"',
)
.header('Cache-Control', 'public, max-age=600');
return { version: '2.0', ...base }; return { version: '2.0', ...base };
}); });

View File

@ -73,7 +73,7 @@ export class WellKnownServerService {
}); });
fastify.get('/.well-known/host-meta.json', async (request, reply) => { fastify.get('/.well-known/host-meta.json', async (request, reply) => {
reply.header('Content-Type', jrd); reply.header('Content-Type', 'application/json');
return { return {
links: [{ links: [{
rel: 'lrdd', rel: 'lrdd',

View File

@ -35,7 +35,7 @@ const accessDenied = {
export class ApiCallService implements OnApplicationShutdown { export class ApiCallService implements OnApplicationShutdown {
private logger: Logger; private logger: Logger;
private userIpHistories: Map<MiUser['id'], Set<string>>; private userIpHistories: Map<MiUser['id'], Set<string>>;
private userIpHistoriesClearIntervalId: NodeJS.Timer; private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor( constructor(
@Inject(DI.userIpsRepository) @Inject(DI.userIpsRepository)

View File

@ -283,6 +283,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pagePush from './endpoints/page-push.js';
import * as ep___pages_create from './endpoints/pages/create.js'; import * as ep___pages_create from './endpoints/pages/create.js';
import * as ep___pages_delete from './endpoints/pages/delete.js'; import * as ep___pages_delete from './endpoints/pages/delete.js';
@ -629,6 +630,7 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default };
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default }; const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default }; const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default }; const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
@ -979,6 +981,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_userListTimeline, $notes_userListTimeline,
$notifications_create, $notifications_create,
$notifications_markAllAsRead, $notifications_markAllAsRead,
$notifications_testNotification,
$pagePush, $pagePush,
$pages_create, $pages_create,
$pages_delete, $pages_delete,

View File

@ -3,22 +3,26 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { randomBytes } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import * as OTPAuth from 'otpauth'; import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js'; import type {
SigninsRepository,
UserProfilesRepository,
UsersRepository,
} from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { getIpHash } from '@/misc/get-ip-hash.js'; import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser } from '@/models/entities/User.js'; import type { MiLocalUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { RateLimiterService } from './RateLimiterService.js'; import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify'; import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
import type { FastifyReply, FastifyRequest } from 'fastify';
@Injectable() @Injectable()
export class SigninApiService { export class SigninApiService {
@ -29,22 +33,16 @@ export class SigninApiService {
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.attestationChallengesRepository)
private attestationChallengesRepository: AttestationChallengesRepository,
@Inject(DI.signinsRepository) @Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository, private signinsRepository: SigninsRepository,
private idService: IdService, private idService: IdService,
private rateLimiterService: RateLimiterService, private rateLimiterService: RateLimiterService,
private signinService: SigninService, private signinService: SigninService,
private twoFactorAuthenticationService: TwoFactorAuthenticationService, private webAuthnService: WebAuthnService,
) { ) {
} }
@ -55,11 +53,7 @@ export class SigninApiService {
username: string; username: string;
password: string; password: string;
token?: string; token?: string;
signature?: string; credential?: AuthenticationResponseJSON;
authenticatorData?: string;
clientDataJSON?: string;
credentialId?: string;
challengeId?: string;
}; };
}>, }>,
reply: FastifyReply, reply: FastifyReply,
@ -181,64 +175,16 @@ export class SigninApiService {
} else { } else {
return this.signinService.signin(request, reply, user); return this.signinService.signin(request, reply, user);
} }
} else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) { } else if (body.credential) {
if (!same && !profile.usePasswordLessLogin) { if (!same && !profile.usePasswordLessLogin) {
return await fail(403, { return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
}); });
} }
const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential);
const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
const challenge = await this.attestationChallengesRepository.findOneBy({
userId: user.id,
id: body.challengeId,
registrationChallenge: false,
challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'),
});
if (!challenge) { if (authorized) {
return await fail(403, {
id: '2715a88a-2125-4013-932f-aa6fe72792da',
});
}
await this.attestationChallengesRepository.delete({
userId: user.id,
id: body.challengeId,
});
if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
return await fail(403, {
id: '2715a88a-2125-4013-932f-aa6fe72792da',
});
}
const securityKey = await this.userSecurityKeysRepository.findOneBy({
id: Buffer.from(
body.credentialId
.replace(/-/g, '+')
.replace(/_/g, '/'),
'base64',
).toString('hex'),
});
if (!securityKey) {
return await fail(403, {
id: '66269679-aeaf-4474-862b-eb761197e046',
});
}
const isValid = this.twoFactorAuthenticationService.verifySignin({
publicKey: Buffer.from(securityKey.publicKey, 'hex'),
authenticatorData: Buffer.from(body.authenticatorData, 'hex'),
clientDataJSON,
clientData,
signature: Buffer.from(body.signature, 'hex'),
challenge: challenge.challenge,
});
if (isValid) {
return this.signinService.signin(request, reply, user); return this.signinService.signin(request, reply, user);
} else { } else {
return await fail(403, { return await fail(403, {
@ -252,42 +198,11 @@ export class SigninApiService {
}); });
} }
const keys = await this.userSecurityKeysRepository.findBy({ const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
userId: user.id,
});
if (keys.length === 0) {
return await fail(403, {
id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4',
});
}
// 32 byte challenge
const challenge = randomBytes(32).toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const challengeId = this.idService.genId();
await this.attestationChallengesRepository.insert({
userId: user.id,
id: challengeId,
challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: false,
});
reply.code(200); reply.code(200);
return { return authRequest;
challenge,
challengeId,
securityKeys: keys.map(key => ({
id: key.id,
})),
};
} }
// never get here // never get here
} }
} }

View File

@ -283,6 +283,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pagePush from './endpoints/page-push.js';
import * as ep___pages_create from './endpoints/pages/create.js'; import * as ep___pages_create from './endpoints/pages/create.js';
import * as ep___pages_delete from './endpoints/pages/delete.js'; import * as ep___pages_delete from './endpoints/pages/delete.js';
@ -627,6 +628,7 @@ const eps = [
['notes/user-list-timeline', ep___notes_userListTimeline], ['notes/user-list-timeline', ep___notes_userListTimeline],
['notifications/create', ep___notifications_create], ['notifications/create', ep___notifications_create],
['notifications/mark-all-as-read', ep___notifications_markAllAsRead], ['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
['notifications/test-notification', ep___notifications_testNotification],
['page-push', ep___pagePush], ['page-push', ep___pagePush],
['pages/create', ep___pages_create], ['pages/create', ep___pages_create],
['pages/delete', ep___pages_delete], ['pages/delete', ep___pages_delete],

View File

@ -3,155 +3,86 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { promisify } from 'node:util';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import cbor from 'cbor';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
import type { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
errors: {
incorrectPassword: {
message: 'Incorrect password.',
code: 'INCORRECT_PASSWORD',
id: '0d7ec6d2-e652-443e-a7bf-9ee9a0cd77b0',
},
twoFactorNotEnabled: {
message: '2fa not enabled.',
code: 'TWO_FACTOR_NOT_ENABLED',
id: '798d6847-b1ed-4f9c-b1f9-163c42655995',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
clientDataJSON: { type: 'string' },
attestationObject: { type: 'string' },
password: { type: 'string' }, password: { type: 'string' },
challengeId: { type: 'string' },
name: { type: 'string', minLength: 1, maxLength: 30 }, name: { type: 'string', minLength: 1, maxLength: 30 },
credential: { type: 'object' },
}, },
required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'], required: ['password', 'name', 'credential'],
} as const; } as const;
// eslint-disable-next-line import/no-default-export
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.userSecurityKeysRepository) @Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository, private userSecurityKeysRepository: UserSecurityKeysRepository,
@Inject(DI.attestationChallengesRepository) private webAuthnService: WebAuthnService,
private attestationChallengesRepository: AttestationChallengesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private twoFactorAuthenticationService: TwoFactorAuthenticationService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const rpIdHashReal = this.twoFactorAuthenticationService.hash(Buffer.from(this.config.hostname, 'utf-8'));
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await bcrypt.compare(ps.password, profile.password ?? '');
if (!same) { if (!same) {
throw new Error('incorrect password'); throw new ApiError(meta.errors.incorrectPassword);
} }
if (!profile.twoFactorEnabled) { if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled'); throw new ApiError(meta.errors.twoFactorNotEnabled);
} }
const clientData = JSON.parse(ps.clientDataJSON); const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential);
if (clientData.type !== 'webauthn.create') {
throw new Error('not a creation attestation');
}
if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
throw new Error('origin mismatch');
}
const clientDataJSONHash = this.twoFactorAuthenticationService.hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
const attestation = await cborDecodeFirst(ps.attestationObject);
const rpIdHash = attestation.authData.slice(0, 32);
if (!rpIdHashReal.equals(rpIdHash)) {
throw new Error('rpIdHash mismatch');
}
const flags = attestation.authData[32];
// eslint:disable-next-line:no-bitwise
if (!(flags & 1)) {
throw new Error('user not present');
}
const authData = Buffer.from(attestation.authData);
const credentialIdLength = authData.readUInt16BE(53);
const credentialId = authData.slice(55, 55 + credentialIdLength);
const publicKeyData = authData.slice(55 + credentialIdLength);
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
if (publicKey.get(3) !== -7) {
throw new Error('alg mismatch');
}
const procedures = this.twoFactorAuthenticationService.getProcedures();
if (!(procedures as any)[attestation.fmt]) {
throw new Error(`unsupported fmt: ${attestation.fmt}. Supported ones: ${Object.keys(procedures)}`);
}
const verificationData = (procedures as any)[attestation.fmt].verify({
attStmt: attestation.attStmt,
authenticatorData: authData,
clientDataHash: clientDataJSONHash,
credentialId,
publicKey,
rpIdHash,
});
if (!verificationData.valid) throw new Error('signature invalid');
const attestationChallenge = await this.attestationChallengesRepository.findOneBy({
userId: me.id,
id: ps.challengeId,
registrationChallenge: true,
challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'),
});
if (!attestationChallenge) {
throw new Error('non-existent challenge');
}
await this.attestationChallengesRepository.delete({
userId: me.id,
id: ps.challengeId,
});
// Expired challenge (> 5min old)
if (
new Date().getTime() - attestationChallenge.createdAt.getTime() >=
5 * 60 * 1000
) {
throw new Error('expired challenge');
}
const credentialIdString = credentialId.toString('hex');
const credentialId = Buffer.from(keyInfo.credentialID).toString('base64url');
await this.userSecurityKeysRepository.insert({ await this.userSecurityKeysRepository.insert({
id: credentialId,
userId: me.id, userId: me.id,
id: credentialIdString,
lastUsed: new Date(),
name: ps.name, name: ps.name,
publicKey: verificationData.publicKey.toString('hex'), publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'),
counter: keyInfo.counter,
credentialDeviceType: keyInfo.credentialDeviceType,
credentialBackedUp: keyInfo.credentialBackedUp,
transports: keyInfo.transports,
}); });
// Publish meUpdated event // Publish meUpdated event
@ -161,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
})); }));
return { return {
id: credentialIdString, id: credentialId,
name: ps.name, name: ps.name,
}; };
}); });

View File

@ -3,22 +3,38 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { promisify } from 'node:util';
import * as crypto from 'node:crypto';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository, AttestationChallengesRepository } from '@/models/index.js'; import type { UserProfilesRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
const randomBytes = promisify(crypto.randomBytes); import { ApiError } from '@/server/api/error.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
errors: {
userNotFound: {
message: 'User not found.',
code: 'USER_NOT_FOUND',
id: '652f899f-66d4-490e-993e-6606c8ec04c3',
},
incorrectPassword: {
message: 'Incorrect password.',
code: 'INCORRECT_PASSWORD',
id: '38769596-efe2-4faf-9bec-abbb3f2cd9ba',
},
twoFactorNotEnabled: {
message: '2fa not enabled.',
code: 'TWO_FACTOR_NOT_ENABLED',
id: 'bf32b864-449b-47b8-974e-f9a5468546f1',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -29,53 +45,43 @@ export const paramDef = {
required: ['password'], required: ['password'],
} as const; } as const;
// eslint-disable-next-line import/no-default-export
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor( constructor(
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.attestationChallengesRepository) private webAuthnService: WebAuthnService,
private attestationChallengesRepository: AttestationChallengesRepository,
private idService: IdService,
private twoFactorAuthenticationService: TwoFactorAuthenticationService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOne({
where: {
userId: me.id,
},
relations: ['user'],
});
if (profile == null) {
throw new ApiError(meta.errors.userNotFound);
}
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await bcrypt.compare(ps.password, profile.password ?? '');
if (!same) { if (!same) {
throw new Error('incorrect password'); throw new ApiError(meta.errors.incorrectPassword);
} }
if (!profile.twoFactorEnabled) { if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled'); throw new ApiError(meta.errors.twoFactorNotEnabled);
} }
// 32 byte challenge return await this.webAuthnService.initiateRegistration(
const entropy = await randomBytes(32); me.id,
const challenge = entropy.toString('base64') profile.user?.username ?? me.id,
.replace(/=/g, '') profile.user?.name ?? undefined,
.replace(/\+/g, '-') );
.replace(/\//g, '_');
const challengeId = this.idService.genId();
await this.attestationChallengesRepository.insert({
userId: me.id,
id: challengeId,
challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: true,
});
return {
challengeId,
challenge,
};
}); });
} }
} }

View File

@ -11,11 +11,20 @@ import type { UserProfilesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
errors: {
incorrectPassword: {
message: 'Incorrect password.',
code: 'INCORRECT_PASSWORD',
id: '78d6c839-20c9-4c66-b90a-fc0542168b48',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -39,10 +48,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await bcrypt.compare(ps.password, profile.password ?? '');
if (!same) { if (!same) {
throw new Error('incorrect password'); throw new ApiError(meta.errors.incorrectPassword);
} }
// Generate user's secret key // Generate user's secret key

View File

@ -10,11 +10,20 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
errors: {
incorrectPassword: {
message: 'Incorrect password.',
code: 'INCORRECT_PASSWORD',
id: '141c598d-a825-44c8-9173-cfb9d92be493',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -42,10 +51,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await bcrypt.compare(ps.password, profile.password ?? '');
if (!same) { if (!same) {
throw new Error('incorrect password'); throw new ApiError(meta.errors.incorrectPassword);
} }
// Make sure we only delete the user's own creds // Make sure we only delete the user's own creds

View File

@ -10,11 +10,20 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/index.js'; import type { UserProfilesRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
errors: {
incorrectPassword: {
message: 'Incorrect password.',
code: 'INCORRECT_PASSWORD',
id: '7add0395-9901-4098-82f9-4f67af65f775',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -38,10 +47,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await bcrypt.compare(ps.password, profile.password ?? '');
if (!same) { if (!same) {
throw new Error('incorrect password'); throw new ApiError(meta.errors.incorrectPassword);
} }
await this.userProfilesRepository.update(me.id, { await this.userProfilesRepository.update(me.id, {

View File

@ -25,7 +25,7 @@ export const meta = {
}, },
accessDenied: { accessDenied: {
message: 'You do not have edit privilege of the channel.', message: 'You do not have edit privilege of this key.',
code: 'ACCESS_DENIED', code: 'ACCESS_DENIED',
id: '1fb7cb09-d46a-4fff-b8df-057708cce513', id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
}, },

View File

@ -34,12 +34,12 @@ export const paramDef = {
properties: { properties: {
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
url: { type: 'string', minLength: 1, maxLength: 1024 }, url: { type: 'string', minLength: 1, maxLength: 1024 },
secret: { type: 'string', minLength: 1, maxLength: 1024 }, secret: { type: 'string', maxLength: 1024, default: '' },
on: { type: 'array', items: { on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes, type: 'string', enum: webhookEventTypes,
} }, } },
}, },
required: ['name', 'url', 'secret', 'on'], required: ['name', 'url', 'on'],
} as const; } as const;
// TODO: ロジックをサービスに切り出す // TODO: ロジックをサービスに切り出す

View File

@ -34,13 +34,13 @@ export const paramDef = {
webhookId: { type: 'string', format: 'misskey:id' }, webhookId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
url: { type: 'string', minLength: 1, maxLength: 1024 }, url: { type: 'string', minLength: 1, maxLength: 1024 },
secret: { type: 'string', minLength: 1, maxLength: 1024 }, secret: { type: 'string', maxLength: 1024, default: '' },
on: { type: 'array', items: { on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes, type: 'string', enum: webhookEventTypes,
} }, } },
active: { type: 'boolean' }, active: { type: 'boolean' },
}, },
required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'], required: ['webhookId', 'name', 'url', 'on', 'active'],
} as const; } as const;
// TODO: ロジックをサービスに切り出す // TODO: ロジックをサービスに切り出す

View File

@ -63,6 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.where(`'{"${me.id}"}' <@ note.mentions`) .where(`'{"${me.id}"}' <@ note.mentions`)
.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`);
})) }))
// Avoid scanning primary key index
.orderBy('CONCAT(note.id)', 'DESC')
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')

View File

@ -69,7 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
//#region Construct query //#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで // パフォーマンス上の利点が無さそう?
//.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')

View File

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NotificationService } from '@/core/NotificationService.js';
export const meta = {
tags: ['notifications'],
requireCredential: true,
kind: 'write:notifications',
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, user) => {
this.notificationService.createNotification(user.id, 'test', {});
});
}
}

View File

@ -80,9 +80,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('note.channel', 'channel')
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser');
query.andWhere(new Brackets(qb => {
qb.orWhere('note.channelId IS NULL');
qb.orWhere('channel.isSensitive = false');
}));
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
if (me) { if (me) {
this.queryService.generateMutedUserQuery(query, me, user); this.queryService.generateMutedUserQuery(query, me, user);

View File

@ -19,7 +19,7 @@ class UserListChannel extends Channel {
public static requireCredential = false; public static requireCredential = false;
private listId: string; private listId: string;
public listUsers: MiUser['id'][] = []; public listUsers: MiUser['id'][] = [];
private listUsersClock: NodeJS.Timer; private listUsersClock: NodeJS.Timeout;
constructor( constructor(
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,

View File

@ -35,7 +35,7 @@ export default class Connection {
public userIdsWhoMeMuting: Set<string> = new Set(); public userIdsWhoMeMuting: Set<string> = new Set();
public userIdsWhoBlockingMe: Set<string> = new Set(); public userIdsWhoBlockingMe: Set<string> = new Set();
public userIdsWhoMeMutingRenotes: Set<string> = new Set(); public userIdsWhoMeMutingRenotes: Set<string> = new Set();
private fetchIntervalId: NodeJS.Timer | null = null; private fetchIntervalId: NodeJS.Timeout | null = null;
constructor( constructor(
private channelsService: ChannelsService, private channelsService: ChannelsService,

View File

@ -37,7 +37,6 @@ import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import manifest from './manifest.json' assert { type: 'json' };
import { FeedService } from './FeedService.js'; import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js'; import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js'; import { ClientLoggerService } from './ClientLoggerService.js';
@ -52,6 +51,45 @@ const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
const viteOut = `${_dirname}/../../../../../built/_vite_/`; const viteOut = `${_dirname}/../../../../../built/_vite_/`;
const manifest = {
'short_name': 'Misskey',
'name': 'Misskey',
'start_url': '/',
'display': 'standalone',
'background_color': '#313a42',
'theme_color': '#86b300',
'icons': [
{
'src': '/static-assets/icons/192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
},
{
'src': '/static-assets/icons/512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
},
{
'src': '/static-assets/splash.png',
'sizes': '300x300',
'type': 'image/png',
'purpose': 'any',
},
],
'share_target': {
'action': '/share/',
'method': 'GET',
'enctype': 'application/x-www-form-urlencoded',
'params': {
'title': 'title',
'text': 'text',
'url': 'url',
},
},
};
@Injectable() @Injectable()
export class ClientServerService { export class ClientServerService {
private logger: Logger; private logger: Logger;

View File

@ -7,15 +7,15 @@ doctype html
// //
- -
_____ _ _ _____ _ _
| |_|___ ___| |_ ___ _ _ | |_|___ ___| |_ ___ _ _
| | | | |_ -|_ -| '_| -_| | | | | | | |_ -|_ -| '_| -_| | |
|_|_|_|_|___|___|_,_|___|_ | |_|_|_|_|___|___|_,_|___|_ |
|___| |___|
Thank you for using Misskey! Thank you for using Misskey!
If you are reading this message... how about joining the development? If you are reading this message... how about joining the development?
https://github.com/misskey-dev/misskey https://github.com/misskey-dev/misskey
html html
@ -35,7 +35,7 @@ html
link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl) link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842 //- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.25.0') link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.32.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists if !config.clientManifestExists

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View File

@ -9,8 +9,16 @@ import * as assert from 'assert';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import cbor from 'cbor'; import cbor from 'cbor';
import * as OTPAuth from 'otpauth'; import * as OTPAuth from 'otpauth';
import { loadConfig } from '../../src/config.js'; import { loadConfig } from '@/config.js';
import { signup, api, post, react, startServer, waitFire } from '../utils.js'; import { api, signup, startServer } from '../utils.js';
import type {
AuthenticationResponseJSON,
AuthenticatorAssertionResponseJSON,
AuthenticatorAttestationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/typescript-types';
import type { INestApplicationContext } from '@nestjs/common'; import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
@ -47,21 +55,18 @@ describe('2要素認証', () => {
const rpIdHash = (): Buffer => { const rpIdHash = (): Buffer => {
return crypto.createHash('sha256') return crypto.createHash('sha256')
.update(Buffer.from(config.hostname, 'utf-8')) .update(Buffer.from(config.host, 'utf-8'))
.digest(); .digest();
}; };
const keyDoneParam = (param: { const keyDoneParam = (param: {
keyName: string, keyName: string,
challengeId: string,
challenge: string,
credentialId: Buffer, credentialId: Buffer,
creationOptions: PublicKeyCredentialCreationOptionsJSON,
}): { }): {
attestationObject: string,
challengeId: string,
clientDataJSON: string,
password: string, password: string,
name: string, name: string,
credential: RegistrationResponseJSON,
} => { } => {
// A COSE encoded public key // A COSE encoded public key
const credentialPublicKey = cbor.encode(new Map<number, unknown>([ const credentialPublicKey = cbor.encode(new Map<number, unknown>([
@ -76,7 +81,7 @@ describe('2要素認証', () => {
// AuthenticatorAssertionResponse.authenticatorData // AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const credentialIdLength = Buffer.allocUnsafe(2); const credentialIdLength = Buffer.allocUnsafe(2);
credentialIdLength.writeUInt16BE(param.credentialId.length); credentialIdLength.writeUInt16BE(param.credentialId.length, 0);
const authData = Buffer.concat([ const authData = Buffer.concat([
rpIdHash(), // rpIdHash(32) rpIdHash(), // rpIdHash(32)
Buffer.from([0x45]), // flags(1) Buffer.from([0x45]), // flags(1)
@ -88,20 +93,27 @@ describe('2要素認証', () => {
]); ]);
return { return {
attestationObject: cbor.encode({
fmt: 'none',
attStmt: {},
authData,
}).toString('hex'),
challengeId: param.challengeId,
clientDataJSON: JSON.stringify({
type: 'webauthn.create',
challenge: param.challenge,
origin: config.scheme + '://' + config.host,
androidPackageName: 'org.mozilla.firefox',
}),
password, password,
name: param.keyName, name: param.keyName,
credential: <RegistrationResponseJSON>{
id: param.credentialId.toString('base64url'),
rawId: param.credentialId.toString('base64url'),
response: <AuthenticatorAttestationResponseJSON>{
clientDataJSON: Buffer.from(JSON.stringify({
type: 'webauthn.create',
challenge: param.creationOptions.challenge,
origin: config.scheme + '://' + config.host,
androidPackageName: 'org.mozilla.firefox',
}), 'utf-8').toString('base64url'),
attestationObject: cbor.encode({
fmt: 'none',
attStmt: {},
authData,
}).toString('base64url'),
},
clientExtensionResults: {},
type: 'public-key',
},
}; };
}; };
@ -121,17 +133,12 @@ describe('2要素認証', () => {
const signinWithSecurityKeyParam = (param: { const signinWithSecurityKeyParam = (param: {
keyName: string, keyName: string,
challengeId: string,
challenge: string,
credentialId: Buffer, credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON,
}): { }): {
authenticatorData: string,
credentialId: string,
challengeId: string,
clientDataJSON: string,
username: string, username: string,
password: string, password: string,
signature: string, credential: AuthenticationResponseJSON,
'g-recaptcha-response'?: string | null, 'g-recaptcha-response'?: string | null,
'hcaptcha-response'?: string | null, 'hcaptcha-response'?: string | null,
} => { } => {
@ -144,10 +151,10 @@ describe('2要素認証', () => {
]); ]);
const clientDataJSONBuffer = Buffer.from(JSON.stringify({ const clientDataJSONBuffer = Buffer.from(JSON.stringify({
type: 'webauthn.get', type: 'webauthn.get',
challenge: param.challenge, challenge: param.requestOptions.challenge,
origin: config.scheme + '://' + config.host, origin: config.scheme + '://' + config.host,
androidPackageName: 'org.mozilla.firefox', androidPackageName: 'org.mozilla.firefox',
})); }), 'utf-8');
const hashedclientDataJSON = crypto.createHash('sha256') const hashedclientDataJSON = crypto.createHash('sha256')
.update(clientDataJSONBuffer) .update(clientDataJSONBuffer)
.digest(); .digest();
@ -156,13 +163,19 @@ describe('2要素認証', () => {
.update(Buffer.concat([authenticatorData, hashedclientDataJSON])) .update(Buffer.concat([authenticatorData, hashedclientDataJSON]))
.sign(privateKey); .sign(privateKey);
return { return {
authenticatorData: authenticatorData.toString('hex'),
credentialId: param.credentialId.toString('base64'),
challengeId: param.challengeId,
clientDataJSON: clientDataJSONBuffer.toString('hex'),
username, username,
password, password,
signature: signature.toString('hex'), credential: <AuthenticationResponseJSON>{
id: param.credentialId.toString('base64url'),
rawId: param.credentialId.toString('base64url'),
response: <AuthenticatorAssertionResponseJSON>{
clientDataJSON: clientDataJSONBuffer.toString('base64url'),
authenticatorData: authenticatorData.toString('base64url'),
signature: signature.toString('base64url'),
},
clientExtensionResults: {},
type: 'public-key',
},
'g-recaptcha-response': null, 'g-recaptcha-response': null,
'hcaptcha-response': null, 'hcaptcha-response': null,
}; };
@ -222,19 +235,18 @@ describe('2要素認証', () => {
password, password,
}, alice); }, alice);
assert.strictEqual(registerKeyResponse.status, 200); assert.strictEqual(registerKeyResponse.status, 200);
assert.notEqual(registerKeyResponse.body.challengeId, undefined); assert.notEqual(registerKeyResponse.body.rp, undefined);
assert.notEqual(registerKeyResponse.body.challenge, undefined); assert.notEqual(registerKeyResponse.body.challenge, undefined);
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
keyName, keyName,
challengeId: registerKeyResponse.body.challengeId,
challenge: registerKeyResponse.body.challenge,
credentialId, credentialId,
creationOptions: registerKeyResponse.body,
}), alice); }), alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('hex')); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName); assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('/users/show', {
@ -248,16 +260,14 @@ describe('2要素認証', () => {
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined); assert.strictEqual(signinResponse.body.i, undefined);
assert.notEqual(signinResponse.body.challengeId, undefined);
assert.notEqual(signinResponse.body.challenge, undefined); assert.notEqual(signinResponse.body.challenge, undefined);
assert.notEqual(signinResponse.body.securityKeys, undefined); assert.notEqual(signinResponse.body.allowCredentials, undefined);
assert.strictEqual(signinResponse.body.securityKeys[0].id, credentialId.toString('hex')); assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({ const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({
keyName, keyName,
challengeId: signinResponse.body.challengeId,
challenge: signinResponse.body.challenge,
credentialId, credentialId,
requestOptions: signinResponse.body,
})); }));
assert.strictEqual(signinResponse2.status, 200); assert.strictEqual(signinResponse2.status, 200);
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
@ -283,9 +293,8 @@ describe('2要素認証', () => {
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
keyName, keyName,
challengeId: registerKeyResponse.body.challengeId,
challenge: registerKeyResponse.body.challenge,
credentialId, credentialId,
creationOptions: registerKeyResponse.body,
}), alice); }), alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
@ -310,9 +319,8 @@ describe('2要素認証', () => {
const signinResponse2 = await api('/signin', { const signinResponse2 = await api('/signin', {
...signinWithSecurityKeyParam({ ...signinWithSecurityKeyParam({
keyName, keyName,
challengeId: signinResponse.body.challengeId,
challenge: signinResponse.body.challenge,
credentialId, credentialId,
requestOptions: signinResponse.body,
}), }),
password: '', password: '',
}); });
@ -340,23 +348,22 @@ describe('2要素認証', () => {
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
keyName, keyName,
challengeId: registerKeyResponse.body.challengeId,
challenge: registerKeyResponse.body.challenge,
credentialId, credentialId,
creationOptions: registerKeyResponse.body,
}), alice); }), alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key'; const renamedKey = 'other-key';
const updateKeyResponse = await api('/i/2fa/update-key', { const updateKeyResponse = await api('/i/2fa/update-key', {
name: renamedKey, name: renamedKey,
credentialId: credentialId.toString('hex'), credentialId: credentialId.toString('base64url'),
}, alice); }, alice);
assert.strictEqual(updateKeyResponse.status, 200); assert.strictEqual(updateKeyResponse.status, 200);
const iResponse = await api('/i', { const iResponse = await api('/i', {
}, alice); }, alice);
assert.strictEqual(iResponse.status, 200); assert.strictEqual(iResponse.status, 200);
const securityKeys = iResponse.body.securityKeysList.filter(s => s.id === credentialId.toString('hex')); const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
assert.strictEqual(securityKeys.length, 1); assert.strictEqual(securityKeys.length, 1);
assert.strictEqual(securityKeys[0].name, renamedKey); assert.strictEqual(securityKeys[0].name, renamedKey);
assert.notEqual(securityKeys[0].lastUsed, undefined); assert.notEqual(securityKeys[0].lastUsed, undefined);
@ -382,9 +389,8 @@ describe('2要素認証', () => {
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
keyName, keyName,
challengeId: registerKeyResponse.body.challengeId,
challenge: registerKeyResponse.body.challenge,
credentialId, credentialId,
creationOptions: registerKeyResponse.body,
}), alice); }), alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);

View File

@ -10,8 +10,8 @@
"declaration": false, "declaration": false,
"sourceMap": true, "sourceMap": true,
"target": "ES2022", "target": "ES2022",
"module": "es2020", "module": "nodenext",
"moduleResolution": "node16", "moduleResolution": "nodenext",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"removeComments": false, "removeComments": false,
"noLib": false, "noLib": false,

Some files were not shown because too many files have changed in this diff Show More