Merge branch 'misskey-dev:master' into master
This commit is contained in:
commit
618c790e9b
22
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
22
.github/ISSUE_TEMPLATE/01_bug-report.md
vendored
@ -39,8 +39,20 @@ Please include errors from the developer console and/or server log files if you
|
|||||||
<!-- Tell us where on the platform it happens -->
|
<!-- Tell us where on the platform it happens -->
|
||||||
<!-- DO NOT WRITE "latest". Please provide the specific version. -->
|
<!-- DO NOT WRITE "latest". Please provide the specific version. -->
|
||||||
|
|
||||||
Misskey version:
|
### 💻 Frontend
|
||||||
PostgreSQL version:
|
* Model and OS of the device(s):
|
||||||
Redis version:
|
<!-- Example: MacBook Pro (14inch, 2021), macOS Ventura 13.4 -->
|
||||||
Your OS:
|
* Browser:
|
||||||
Your browser:
|
<!-- Example: Chrome 113.0.5672.126 -->
|
||||||
|
* Server URL:
|
||||||
|
<!-- Example: misskey.io -->
|
||||||
|
|
||||||
|
### 🛰 Backend (for instance admin)
|
||||||
|
<!-- If you are using a managed service, put that after the version. -->
|
||||||
|
|
||||||
|
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
|
||||||
|
* Misskey: 13.x.x
|
||||||
|
* Node: 18.x.x
|
||||||
|
* PostgreSQL: 15.x.x
|
||||||
|
* Redis: 7.x.x
|
||||||
|
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->
|
||||||
|
34
.github/workflows/storybook.yml
vendored
34
.github/workflows/storybook.yml
vendored
@ -16,12 +16,22 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.3.0
|
- uses: actions/checkout@v3.3.0
|
||||||
|
if: github.event_name != 'pull_request_target'
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Checkout HEAD
|
- uses: actions/checkout@v3.3.0
|
||||||
if: github.event_name == 'pull_request_target'
|
if: github.event_name == 'pull_request_target'
|
||||||
run: git checkout ${{ github.head_ref }}
|
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
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
@ -68,7 +78,7 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request_target'
|
if: github.event_name == 'pull_request_target'
|
||||||
id: chromatic_pull_request
|
id: chromatic_pull_request
|
||||||
run: |
|
run: |
|
||||||
DIFF="${{ github.base_ref }} HEAD"
|
DIFF="${{ steps.rev.outputs.base }} HEAD"
|
||||||
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
|
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
|
||||||
DIFF="HEAD"
|
DIFF="HEAD"
|
||||||
fi
|
fi
|
||||||
@ -76,7 +86,11 @@ jobs:
|
|||||||
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
|
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static $(echo "$CHROMATIC_PARAMETER")
|
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:
|
env:
|
||||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
- name: Notify that Chromatic detects changes
|
- name: Notify that Chromatic detects changes
|
||||||
@ -91,18 +105,6 @@ jobs:
|
|||||||
commit_sha: context.sha,
|
commit_sha: context.sha,
|
||||||
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
|
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
|
||||||
})
|
})
|
||||||
- name: Notify that Chromatic will skip testing
|
|
||||||
uses: actions/github-script@v6.4.0
|
|
||||||
if: github.event_name == 'pull_request_target' && steps.chromatic_pull_request.outputs.skip == 'true'
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
github.rest.issues.createComment({
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
body: 'Chromatic will skip testing but you may still have to [review the changes on Chromatic](https://www.chromatic.com/pullrequests?appId=6428f7d7b962f0b79f97d6e4).'
|
|
||||||
})
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
48
CHANGELOG.md
48
CHANGELOG.md
@ -12,6 +12,52 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 13.13.1
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Fix: タブがアクティブな間はstreamが切断されないように
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Fix: api/metaで`TypeError: JSON5.parse is not a function`エラーが発生する問題を修正
|
||||||
|
|
||||||
|
## 13.13.0
|
||||||
|
|
||||||
|
### General
|
||||||
|
- カスタム絵文字ごとにそれをリアクションとして使えるロールを設定できるように
|
||||||
|
- カスタム絵文字ごとに連合するかどうか設定できるように
|
||||||
|
- カスタム絵文字ごとにセンシティブフラグを設定できるように
|
||||||
|
- センシティブなカスタム絵文字のリアクションを受け入れない設定が可能に
|
||||||
|
- タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように
|
||||||
|
- 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります
|
||||||
|
- リストを公開できるようになりました
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- リアクションの取り消し/変更時に確認ダイアログを出すように
|
||||||
|
- 開発者モードを追加
|
||||||
|
- AiScriptを0.13.3に更新
|
||||||
|
- Deck UIを使用している場合、`/`以外にアクセスした際にZen UIで表示するように
|
||||||
|
- メインカラムを設置していない場合の問題を解決
|
||||||
|
- ハッシュタグのノート一覧ページから、そのハッシュタグで投稿するボタンを追加
|
||||||
|
- アカウント初期設定ウィザードに戻るボタンを追加
|
||||||
|
- アカウントの初期設定ウィザードにあとでボタンを追加
|
||||||
|
- サーバーにカスタム絵文字の種類が多い場合のパフォーマンスの改善
|
||||||
|
- Fix: URLプレビューで情報が取得できなかった際の挙動を修正
|
||||||
|
- Fix: Safari、Firefoxでの新規登録時、パスワードマネージャーにメールアドレスが登録されていた挙動を修正
|
||||||
|
- Fix: ロールタイムラインが無効でも投稿が流れてしまう問題の修正
|
||||||
|
- Fix: ロールタイムラインにて全ての投稿が流れてしまう問題の修正
|
||||||
|
- Fix: 「アクセストークンの管理」画面でアプリの情報が表示されない問題の修正
|
||||||
|
- Fix: Firefoxにおける絵文字ピッカーのTabキーフォーカス問題の修正
|
||||||
|
- Fix: フォローボタンがテーマのカラースキームによって視認性が悪くなる問題を修正
|
||||||
|
- 新しいプロパティ `fgOnWhite` が追加されました
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善
|
||||||
|
- ストリーミングのパフォーマンスを改善
|
||||||
|
- Fix: 無効化されたアンテナにアクセスがあった際に再度有効化するように
|
||||||
|
- Fix: お知らせの画像URLを空にできない問題を修正
|
||||||
|
- Fix: i/notificationsのsinceIdが機能しない問題を修正
|
||||||
|
- Fix: pageのピン留めを解除することができない問題を修正
|
||||||
|
|
||||||
## 13.12.2
|
## 13.12.2
|
||||||
|
|
||||||
## NOTE
|
## NOTE
|
||||||
@ -87,6 +133,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
|
|||||||
* 画像が全て隠れた状態で表示されるようになります
|
* 画像が全て隠れた状態で表示されるようになります
|
||||||
- 閲覧注意設定された画像は表示した状態でもそれが閲覧注意だと分かる表示をするように
|
- 閲覧注意設定された画像は表示した状態でもそれが閲覧注意だと分かる表示をするように
|
||||||
- モデレーターはノートに添付された画像上から直接NSFW設定できるように
|
- モデレーターはノートに添付された画像上から直接NSFW設定できるように
|
||||||
|
- 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にするように
|
||||||
- プロフィール設定「追加情報」の項目の削除と並び替えができるように
|
- プロフィール設定「追加情報」の項目の削除と並び替えができるように
|
||||||
- 新しい実績を追加
|
- 新しい実績を追加
|
||||||
- AiScriptを0.13.2に更新
|
- AiScriptを0.13.2に更新
|
||||||
@ -334,6 +381,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
|
|||||||
- アンテナでCWも検索対象にするように
|
- アンテナでCWも検索対象にするように
|
||||||
- ノートの操作部をホバー時のみ表示するオプションを追加
|
- ノートの操作部をホバー時のみ表示するオプションを追加
|
||||||
- サウンドを追加
|
- サウンドを追加
|
||||||
|
- enhance(client): MFMのx2, scale, positionが含まれていたらノートをたたむように
|
||||||
- サーバーのパフォーマンスを改善
|
- サーバーのパフォーマンスを改善
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
@ -21,6 +21,8 @@ import './commands'
|
|||||||
|
|
||||||
Cypress.on('uncaught:exception', (err, runnable) => {
|
Cypress.on('uncaught:exception', (err, runnable) => {
|
||||||
if ([
|
if ([
|
||||||
|
'The source image cannot be decoded',
|
||||||
|
|
||||||
// Chrome
|
// Chrome
|
||||||
'ResizeObserver loop limit exceeded',
|
'ResizeObserver loop limit exceeded',
|
||||||
|
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
DONATORS
|
|
||||||
========
|
|
||||||
The list of people who have sent donation for Misskey.
|
|
||||||
|
|
||||||
(In random order, honorific titles are omitted.)
|
|
||||||
|
|
||||||
* らふぁ
|
|
||||||
* 俺様
|
|
||||||
* なぎうり
|
|
||||||
* スルメ https://surume.tk/
|
|
||||||
* 藍
|
|
||||||
* 音船 https://otofune.me/
|
|
||||||
* aqz https://misskey.xyz/aqz
|
|
||||||
* kotodu "虚無創作中"
|
|
||||||
* Maya Minatsuki
|
|
||||||
* Knzk https://knzk.me/@Knzk
|
|
||||||
* ねじりわさび https://knzk.me/@y
|
|
||||||
* NCLS https://knzk.me/@imncls]
|
|
||||||
* こじま @skoji@sandbox.skoji.jp
|
|
||||||
|
|
||||||
:heart: Thanks for donating, guys!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
If your name is missing, please contact us!
|
|
@ -2,6 +2,7 @@
|
|||||||
_lang_: "العربية"
|
_lang_: "العربية"
|
||||||
headlineMisskey: "شبكة مرتبطة بالملاحظات"
|
headlineMisskey: "شبكة مرتبطة بالملاحظات"
|
||||||
introMisskey: "اهلا بك! ميسكي هو منصة تدوين مصغر لا مركزية ومفتوحة المصدر.\nيمكنك مشاركة \"ملاحظات\" عن ما يجري حولك، وإخبار الجميع عن نفسك 📡\nتسمح لك \"الانفعالات\" بتعبير عن شعورك حول ملاحظات الآخرين 👍\nاكتشف عالمًا جديدًا 🚀"
|
introMisskey: "اهلا بك! ميسكي هو منصة تدوين مصغر لا مركزية ومفتوحة المصدر.\nيمكنك مشاركة \"ملاحظات\" عن ما يجري حولك، وإخبار الجميع عن نفسك 📡\nتسمح لك \"الانفعالات\" بتعبير عن شعورك حول ملاحظات الآخرين 👍\nاكتشف عالمًا جديدًا 🚀"
|
||||||
|
poweredByMisskeyDescription: "{name} هو إحدى الخِدمات التي تستخدم المنصة مفتوحة المصدر <b>ميسكي</b> (يشار إليه كمثيل ميسكي)"
|
||||||
monthAndDay: "{day}/{month}"
|
monthAndDay: "{day}/{month}"
|
||||||
search: "البحث"
|
search: "البحث"
|
||||||
notifications: "الإشعارات"
|
notifications: "الإشعارات"
|
||||||
@ -49,6 +50,7 @@ deleteAndEdit: "إزالة وإعادة الصياغة"
|
|||||||
deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها."
|
deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها."
|
||||||
addToList: "أضفه إلى قائمة"
|
addToList: "أضفه إلى قائمة"
|
||||||
sendMessage: "أرسل رسالة"
|
sendMessage: "أرسل رسالة"
|
||||||
|
copyRSS: "انسخ رابط RSS"
|
||||||
copyUsername: "انسخ اسم المستخدم"
|
copyUsername: "انسخ اسم المستخدم"
|
||||||
searchUser: "ابحث عن مستخدمين"
|
searchUser: "ابحث عن مستخدمين"
|
||||||
reply: "رد"
|
reply: "رد"
|
||||||
@ -102,6 +104,8 @@ renoted: "أُعيد نشره"
|
|||||||
cantRenote: "لا يمكن إعادة نشر الملاحظة"
|
cantRenote: "لا يمكن إعادة نشر الملاحظة"
|
||||||
cantReRenote: "لا يمكنك إعادة نشر ملاحظة معاد نشرها"
|
cantReRenote: "لا يمكنك إعادة نشر ملاحظة معاد نشرها"
|
||||||
quote: "اقتبس"
|
quote: "اقتبس"
|
||||||
|
inChannelRenote: "إعادة نشر في قناة"
|
||||||
|
inChannelQuote: "اقتباس في قناة"
|
||||||
pinnedNote: "ملاحظة مدبسة"
|
pinnedNote: "ملاحظة مدبسة"
|
||||||
pinned: "دبّسها على الصفحة الشخصية"
|
pinned: "دبّسها على الصفحة الشخصية"
|
||||||
you: "أنت"
|
you: "أنت"
|
||||||
@ -119,6 +123,8 @@ unmarkAsSensitive: "ألغ تعيينه كمحتوى حساس"
|
|||||||
enterFileName: "ادخل اسم الملف"
|
enterFileName: "ادخل اسم الملف"
|
||||||
mute: "اكتم"
|
mute: "اكتم"
|
||||||
unmute: "إلغاء الكتم"
|
unmute: "إلغاء الكتم"
|
||||||
|
renoteMute: "اكتم إعادة النشر"
|
||||||
|
renoteUnmute: "ارفع الكتم عن إعادة النشر"
|
||||||
block: "احجب"
|
block: "احجب"
|
||||||
unblock: "إلغاء الحجب"
|
unblock: "إلغاء الحجب"
|
||||||
suspend: "علِق"
|
suspend: "علِق"
|
||||||
@ -141,6 +147,7 @@ emojiUrl: "رابط الإيموجي"
|
|||||||
addEmoji: "إضافة إيموجي"
|
addEmoji: "إضافة إيموجي"
|
||||||
settingGuide: "الإعدادات المستحسنة"
|
settingGuide: "الإعدادات المستحسنة"
|
||||||
cacheRemoteFiles: "خزن مؤقتا الملفات البعيدة"
|
cacheRemoteFiles: "خزن مؤقتا الملفات البعيدة"
|
||||||
|
cacheRemoteFilesDescription: "إذا عُطل هذا الإعداد، ستُحمل الملفات من المثيل البعيد، هذا سيقلل من المساحة المستغلة على القرص لكن سيزيد حجم تدفق البيانات وهذا لأن الصور المصغرة لن تولّد."
|
||||||
flagAsBot: "علّمه كحساب آلي"
|
flagAsBot: "علّمه كحساب آلي"
|
||||||
flagAsBotDescription: "فعّل هذا الخيار إذا كان هذا الحساب يُدار عبر برمجية. إذا فُعل فسيكون بمثابة علامة للمطورين الآخرين لتجنب سلاسل لا متناهية من التفاعل بين حسابات الآلية وضبط أنظمة ميسكي للتعامل مع هذا الحساب كآلي."
|
flagAsBotDescription: "فعّل هذا الخيار إذا كان هذا الحساب يُدار عبر برمجية. إذا فُعل فسيكون بمثابة علامة للمطورين الآخرين لتجنب سلاسل لا متناهية من التفاعل بين حسابات الآلية وضبط أنظمة ميسكي للتعامل مع هذا الحساب كآلي."
|
||||||
flagAsCat: "علّم هذا الحساب كحساب قط"
|
flagAsCat: "علّم هذا الحساب كحساب قط"
|
||||||
@ -253,14 +260,15 @@ startMessaging: "ابدأ محادثة"
|
|||||||
nUsersRead: "قرأه {n}"
|
nUsersRead: "قرأه {n}"
|
||||||
agreeTo: "اوافق على {0}"
|
agreeTo: "اوافق على {0}"
|
||||||
agree: "أقبل"
|
agree: "أقبل"
|
||||||
|
agreeBelow: "أقبل ما يلي"
|
||||||
basicNotesBeforeCreateAccount: "ملاحظات مهمة"
|
basicNotesBeforeCreateAccount: "ملاحظات مهمة"
|
||||||
termsOfService: "شروط الخدمة"
|
termsOfService: "شروط الخدمة"
|
||||||
start: "البداية"
|
start: "البداية"
|
||||||
home: "الرئيسي"
|
home: "الرئيسي"
|
||||||
remoteUserCaution: "هذه المعلومات قد لا تكون مكتملة بما أن المستخدم من مثيل بعيد."
|
remoteUserCaution: "هذه المعلومات قد لا تكون مكتملة بما أن المستخدم من مثيل بعيد."
|
||||||
activity: "النشاط"
|
activity: "النشاط"
|
||||||
images: "الصور"
|
images: "صور"
|
||||||
image: "الصور"
|
image: "صور"
|
||||||
birthday: "تاريخ الميلاد"
|
birthday: "تاريخ الميلاد"
|
||||||
yearsOld: "{age} سنة"
|
yearsOld: "{age} سنة"
|
||||||
registeredDate: "انضم في"
|
registeredDate: "انضم في"
|
||||||
@ -362,6 +370,7 @@ antennaExcludeKeywords: "الكلمات المفتاحية المستثناة"
|
|||||||
antennaKeywordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\""
|
antennaKeywordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\""
|
||||||
notifyAntenna: "نبهني بصول ملاحظات جديدة"
|
notifyAntenna: "نبهني بصول ملاحظات جديدة"
|
||||||
withFileAntenna: "ملاحظات تحوي ملفات فقط"
|
withFileAntenna: "ملاحظات تحوي ملفات فقط"
|
||||||
|
enableServiceworker: "فعّل إرسال الإشعارات للمتصفح"
|
||||||
antennaUsersDescription: "اكتب اسم مستخدم لكل سطر"
|
antennaUsersDescription: "اكتب اسم مستخدم لكل سطر"
|
||||||
caseSensitive: "حساسية حالة الأحرف"
|
caseSensitive: "حساسية حالة الأحرف"
|
||||||
withReplies: "بالردود"
|
withReplies: "بالردود"
|
||||||
@ -391,6 +400,7 @@ moderation: "الإشراف"
|
|||||||
nUsersMentioned: "{n} مستخدمين أُشير إليهم"
|
nUsersMentioned: "{n} مستخدمين أُشير إليهم"
|
||||||
securityKey: "مفتاح الأمان"
|
securityKey: "مفتاح الأمان"
|
||||||
lastUsed: "آخر استخدام"
|
lastUsed: "آخر استخدام"
|
||||||
|
lastUsedAt: "آخر استخدام: {t}"
|
||||||
unregister: "إلغاء التسجيل"
|
unregister: "إلغاء التسجيل"
|
||||||
passwordLessLogin: "لِج مِن دون كلمة سرية"
|
passwordLessLogin: "لِج مِن دون كلمة سرية"
|
||||||
resetPassword: "أعد تعيين كلمتك السرية"
|
resetPassword: "أعد تعيين كلمتك السرية"
|
||||||
@ -441,6 +451,7 @@ or: "أو"
|
|||||||
language: "اللغة"
|
language: "اللغة"
|
||||||
uiLanguage: "لغة واجهة المستخدم"
|
uiLanguage: "لغة واجهة المستخدم"
|
||||||
aboutX: "عن {x}"
|
aboutX: "عن {x}"
|
||||||
|
emojiStyle: "نمط الوجوه التعبيرية"
|
||||||
noHistory: "السجل فارغ"
|
noHistory: "السجل فارغ"
|
||||||
signinHistory: "تاريخ تسجيل الدخول"
|
signinHistory: "تاريخ تسجيل الدخول"
|
||||||
doing: "انتظر لحظة"
|
doing: "انتظر لحظة"
|
||||||
@ -451,6 +462,7 @@ createAccount: "أنشئ حسابًا"
|
|||||||
existingAccount: "الحسابات الموجودة"
|
existingAccount: "الحسابات الموجودة"
|
||||||
regenerate: "أعِد التوليد"
|
regenerate: "أعِد التوليد"
|
||||||
fontSize: "حجم الخط"
|
fontSize: "حجم الخط"
|
||||||
|
limitTo: "سقفهُ لـ{x}"
|
||||||
noFollowRequests: "ليس لديك طلبات متابعة معلقة"
|
noFollowRequests: "ليس لديك طلبات متابعة معلقة"
|
||||||
openImageInNewTab: "إفتح الصورة بصفحة جديدة"
|
openImageInNewTab: "إفتح الصورة بصفحة جديدة"
|
||||||
dashboard: "لوحة التحكم"
|
dashboard: "لوحة التحكم"
|
||||||
@ -479,6 +491,7 @@ objectStorageUseProxyDesc: "عطل هذا الخيار إذا لم ترد است
|
|||||||
serverLogs: "سجلات الخادم"
|
serverLogs: "سجلات الخادم"
|
||||||
deleteAll: "حذف الكل"
|
deleteAll: "حذف الكل"
|
||||||
showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة"
|
showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة"
|
||||||
|
showFixedPostFormInChannel: "أظهر نموذج الكتابة في أعلى الخط الزمني (قنوات)"
|
||||||
newNoteRecived: "هناك ملاحظات جديدة"
|
newNoteRecived: "هناك ملاحظات جديدة"
|
||||||
sounds: "الرنات"
|
sounds: "الرنات"
|
||||||
sound: "الرنات"
|
sound: "الرنات"
|
||||||
@ -514,6 +527,7 @@ userSilenced: "كُتم هذا المستخدم."
|
|||||||
yourAccountSuspendedTitle: "هذا الحساب معلق"
|
yourAccountSuspendedTitle: "هذا الحساب معلق"
|
||||||
yourAccountSuspendedDescription: "عُلق الحساب بسبب انتهاك شروط خدمة المثيل و ما شابه. إذا أردت معرفة التفصيل تواصل مع مدير المثيل. رجاءً لا تنشئ حساب جديد."
|
yourAccountSuspendedDescription: "عُلق الحساب بسبب انتهاك شروط خدمة المثيل و ما شابه. إذا أردت معرفة التفصيل تواصل مع مدير المثيل. رجاءً لا تنشئ حساب جديد."
|
||||||
accountDeleted: "حُذف الحساب"
|
accountDeleted: "حُذف الحساب"
|
||||||
|
accountDeletedDescription: "حُذف هذا الحساب."
|
||||||
menu: "القائمة"
|
menu: "القائمة"
|
||||||
divider: "فاصل"
|
divider: "فاصل"
|
||||||
addItem: "إضافة عنصر"
|
addItem: "إضافة عنصر"
|
||||||
@ -539,6 +553,7 @@ author: "الكاتب"
|
|||||||
leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟"
|
leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟"
|
||||||
manage: "إدارة "
|
manage: "إدارة "
|
||||||
plugins: "الإضافات"
|
plugins: "الإضافات"
|
||||||
|
preferencesBackups: "النُسخ الاحتياطية للإعدادات"
|
||||||
useFullReactionPicker: "استخدم الحجم الكامل لمنتقي التفاعلات"
|
useFullReactionPicker: "استخدم الحجم الكامل لمنتقي التفاعلات"
|
||||||
width: "العرض"
|
width: "العرض"
|
||||||
height: "الإرتفاع"
|
height: "الإرتفاع"
|
||||||
@ -648,6 +663,7 @@ contact: "التواصل"
|
|||||||
useSystemFont: "استخدم الخط الافتراضية للنظام"
|
useSystemFont: "استخدم الخط الافتراضية للنظام"
|
||||||
clips: "مشابك"
|
clips: "مشابك"
|
||||||
experimentalFeatures: "ميّزات اختبارية"
|
experimentalFeatures: "ميّزات اختبارية"
|
||||||
|
experimental: "اختباري"
|
||||||
developer: "المطور"
|
developer: "المطور"
|
||||||
makeExplorable: "أظهر الحساب في صفحة \"استكشاف\""
|
makeExplorable: "أظهر الحساب في صفحة \"استكشاف\""
|
||||||
makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\""
|
makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\""
|
||||||
@ -669,6 +685,7 @@ accentColor: "طابع لوني"
|
|||||||
textColor: "لون النص"
|
textColor: "لون النص"
|
||||||
saveAs: "احفظ كـ..."
|
saveAs: "احفظ كـ..."
|
||||||
advanced: "متقدم"
|
advanced: "متقدم"
|
||||||
|
advancedSettings: "إعدادات متقدمة"
|
||||||
value: "القيمة"
|
value: "القيمة"
|
||||||
createdAt: "أُنشئ في"
|
createdAt: "أُنشئ في"
|
||||||
updatedAt: "حُدّث في"
|
updatedAt: "حُدّث في"
|
||||||
@ -733,6 +750,7 @@ popularPosts: "المشاركات المتداولة"
|
|||||||
shareWithNote: "شاركه في ملاحظة"
|
shareWithNote: "شاركه في ملاحظة"
|
||||||
ads: "الإعلانات"
|
ads: "الإعلانات"
|
||||||
expiration: "ينتهي استطلاع الرأي في"
|
expiration: "ينتهي استطلاع الرأي في"
|
||||||
|
startingperiod: "ابدأ"
|
||||||
memo: "تذكير"
|
memo: "تذكير"
|
||||||
priority: "الأولوية"
|
priority: "الأولوية"
|
||||||
high: "عالية"
|
high: "عالية"
|
||||||
@ -763,6 +781,7 @@ lastCommunication: "آخر تواصل"
|
|||||||
resolved: "عولج"
|
resolved: "عولج"
|
||||||
unresolved: "لم يعالج"
|
unresolved: "لم يعالج"
|
||||||
breakFollow: "إلغاء الاشتراك"
|
breakFollow: "إلغاء الاشتراك"
|
||||||
|
breakFollowConfirm: "أمتأكد من إزالة المتابِع ؟"
|
||||||
itsOn: "مفعّل"
|
itsOn: "مفعّل"
|
||||||
itsOff: "معطّل"
|
itsOff: "معطّل"
|
||||||
emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
|
emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
|
||||||
@ -777,6 +796,7 @@ muteThread: "اكتم النقاش"
|
|||||||
unmuteThread: "ارفع الكتم عن النقاش"
|
unmuteThread: "ارفع الكتم عن النقاش"
|
||||||
ffVisibility: "مرئية المتابِعين/المتابَعين"
|
ffVisibility: "مرئية المتابِعين/المتابَعين"
|
||||||
ffVisibilityDescription: "يسمح لك بتحديد من يمكنهم رؤية متابِعيك ومتابَعيك."
|
ffVisibilityDescription: "يسمح لك بتحديد من يمكنهم رؤية متابِعيك ومتابَعيك."
|
||||||
|
continueThread: "اعرض بقية النقاش"
|
||||||
deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟"
|
deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟"
|
||||||
incorrectPassword: "كلمة السر خاطئة."
|
incorrectPassword: "كلمة السر خاطئة."
|
||||||
voteConfirm: "متيقِّن من تصويتك لـ {choice}؟"
|
voteConfirm: "متيقِّن من تصويتك لـ {choice}؟"
|
||||||
@ -798,25 +818,127 @@ tenMinutes: "10 دقائق"
|
|||||||
oneHour: "ساعة"
|
oneHour: "ساعة"
|
||||||
oneDay: "يوم"
|
oneDay: "يوم"
|
||||||
oneWeek: "أسبوع"
|
oneWeek: "أسبوع"
|
||||||
|
oneMonth: "شهر"
|
||||||
failedToFetchAccountInformation: "تعذر جلب معلومات الحساب"
|
failedToFetchAccountInformation: "تعذر جلب معلومات الحساب"
|
||||||
|
cropNo: "استخدمها كما هي"
|
||||||
file: "الملفات"
|
file: "الملفات"
|
||||||
|
recentNHours: "آخر {n} ساعة"
|
||||||
|
recentNDays: "آخر {n} أيام"
|
||||||
|
noEmailServerWarning: "خادم البريد غير مضبوط."
|
||||||
|
thereIsUnresolvedAbuseReportWarning: "توجد بلاغات غير معالجة."
|
||||||
|
recommended: "مقترح"
|
||||||
|
driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم"
|
||||||
|
driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل."
|
||||||
|
requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير."
|
||||||
|
typeToConfirm: "أدخل {x} للتأكيد"
|
||||||
|
deleteAccount: "احذف الحساب"
|
||||||
|
document: "التوثيق"
|
||||||
|
numberOfPageCache: "عدد الصفحات المخزنة مؤقتًا"
|
||||||
|
logoutConfirm: "أتريد الخروج؟"
|
||||||
|
lastActiveDate: "آخر استخدام"
|
||||||
|
statusbar: "شريط الحالة"
|
||||||
|
pleaseSelect: "حدد خيارًا"
|
||||||
reverse: "اقلب"
|
reverse: "اقلب"
|
||||||
colored: "ملوّن"
|
colored: "ملوّن"
|
||||||
label: "التسمية"
|
label: "التسمية"
|
||||||
|
type: "نوع"
|
||||||
|
speed: "سرعة"
|
||||||
|
slow: "بطيء"
|
||||||
|
fast: "سريع"
|
||||||
|
sensitiveMediaDetection: "التعرف على المحتوى الحساس"
|
||||||
localOnly: "المحلي فقط"
|
localOnly: "المحلي فقط"
|
||||||
|
failedToUpload: "فشل الرفع"
|
||||||
|
cannotUploadBecauseInappropriate: "تعذر رفع الملف لوجود محتوى حساس فيه."
|
||||||
|
cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين."
|
||||||
|
cannotUploadBecauseExceedsFileSizeLimit: "تعذر رفع الملف بسبب تجاوز حجمه للحد المسموح"
|
||||||
|
beta: "بيتا"
|
||||||
|
navbar: "شريط التنقل"
|
||||||
|
shuffle: "خلط"
|
||||||
account: "الحسابات"
|
account: "الحسابات"
|
||||||
|
move: "أنقل"
|
||||||
|
pushNotification: "إرسال الإشعارات"
|
||||||
|
subscribePushNotification: "فعّل إرسال الإشعارات"
|
||||||
|
unsubscribePushNotification: "عطل إرسال الإشعارات"
|
||||||
|
pushNotificationAlreadySubscribed: "إرسال الإشعارات مفعل سلفًا"
|
||||||
|
pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها."
|
||||||
|
sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها"
|
||||||
|
sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك."
|
||||||
|
caption: "التعليق التوضيحي"
|
||||||
|
tools: "أدوات"
|
||||||
cannotLoad: "تعذر التحميل"
|
cannotLoad: "تعذر التحميل"
|
||||||
like: "أعجبني"
|
like: "أعجبني"
|
||||||
|
unlike: "ألغِ الإعجاب"
|
||||||
show: "المظهر"
|
show: "المظهر"
|
||||||
|
neverShow: "لا تظهره مجددًا"
|
||||||
|
didYouLikeMisskey: "هل أعجبك ميسكي؟"
|
||||||
|
roles: "الأدوار"
|
||||||
|
role: "الدور"
|
||||||
|
noRole: "لم يُعثر على دور"
|
||||||
|
normalUser: "مستخدم عادي"
|
||||||
|
undefined: "غير معرّف"
|
||||||
color: "اللون"
|
color: "اللون"
|
||||||
|
manageCustomEmojis: "إدارة الإيموجي المخصصة"
|
||||||
|
cannotPerformTemporary: "غير متاح مؤقتاً"
|
||||||
|
permissionDeniedError: "رُفضة العملية"
|
||||||
|
preset: "إعدادات مسبقة"
|
||||||
|
selectFromPresets: "اختر من الإعدادات المسبقة"
|
||||||
|
achievements: "الإنجازات"
|
||||||
|
gotInvalidResponseError: "استجابة غير متوقعة من الخادم"
|
||||||
|
gotInvalidResponseErrorDescription: "يتعذر الوصول إلى الخادم أوأنه يُصان، رجاءً حاول لاحقًا."
|
||||||
|
thisPostMayBeAnnoying: "هذا قد يزعج الآخرين."
|
||||||
|
thisPostMayBeAnnoyingHome: "أنشر في الخط الزمني الرئيس"
|
||||||
|
thisPostMayBeAnnoyingCancel: "ألغِ"
|
||||||
|
internalServerError: "خطأ داخلي في الخادم"
|
||||||
|
internalServerErrorDescription: "واجه الخادم خطأ غي متوقع."
|
||||||
|
copyErrorInfo: "انسخ تفاصيل الخطأ"
|
||||||
|
joinThisServer: "سجل في هذا المثيل"
|
||||||
|
exploreOtherServers: "اعثر على مثيل آخر"
|
||||||
|
disableFederationOk: "عطّل"
|
||||||
|
invitationRequiredToRegister: "هذا المثيل للمدعوين فقط. لتسجيل فيه تحتاج رمزًا صالحًا."
|
||||||
|
postToTheChannel: "انشر في قناة"
|
||||||
|
cannotBeChangedLater: "لا يمكن تغييره لاحقًا."
|
||||||
|
reactionAcceptance: "قبول التفاعلات"
|
||||||
|
rolesAssignedToMe: "الأدوار المسندة إلي"
|
||||||
|
resetPasswordConfirm: "هل تريد إعادة تعيين كلمة السر؟"
|
||||||
|
noteIdOrUrl: "معرف الملاحظة أو رابطها"
|
||||||
|
video: "فيديو"
|
||||||
|
videos: "فيديوهات"
|
||||||
|
accountMigration: "ترحيل الحساب"
|
||||||
|
accountMoved: "نقل هذا المستخدم حسابه:"
|
||||||
|
accountMovedShort: "رُحل هذا الحساب."
|
||||||
|
operationForbidden: "عملية ممنوعة"
|
||||||
|
forceShowAds: "أظهر الإعلانات التجارية دائما"
|
||||||
|
vertical: "عمودي"
|
||||||
horizontal: "جانبي"
|
horizontal: "جانبي"
|
||||||
|
position: "الموضع"
|
||||||
|
serverRules: "قوانين الخادم"
|
||||||
|
pleaseConfirmBelowBeforeSignup: "رجاءً وافق على ما يلي قبل التسجيل."
|
||||||
|
pleaseAgreeAllToContinue: "للمتابعة وافق على الحقول أعلاه."
|
||||||
|
continue: "متابعة"
|
||||||
|
preservedUsernames: "أسماء المستخدمين المحجوزة"
|
||||||
|
preservedUsernamesDescription: "قائمة بأسماء المستخدمين المحجوزة كلٌ في سطر. لن يُقبل التسجيل بهذه الأسماء وستبقى محصورة على التسجيل اليدوي بواسطة المديرين. لن يتأثر المستخدمون الذين يملكون هذه الأسماء سلفًا."
|
||||||
|
archive: "الأرشيف"
|
||||||
youFollowing: "متابَع"
|
youFollowing: "متابَع"
|
||||||
|
options: "خيارات"
|
||||||
_role:
|
_role:
|
||||||
|
new: "دور جديد"
|
||||||
|
edit: "حرر الأدوار"
|
||||||
|
name: "اسم الدور"
|
||||||
|
description: "وصف الدور"
|
||||||
|
permission: "أذونات الدور"
|
||||||
|
assignTarget: "نوع الإسناد"
|
||||||
|
options: "خيارات"
|
||||||
|
policies: "السياسة العامة"
|
||||||
priority: "الأولوية"
|
priority: "الأولوية"
|
||||||
_priority:
|
_priority:
|
||||||
low: "منخفضة"
|
low: "منخفضة"
|
||||||
middle: "متوسط"
|
middle: "متوسط"
|
||||||
high: "عالية"
|
high: "عالية"
|
||||||
|
_options:
|
||||||
|
canManageCustomEmojis: "إدارة الإيموجي المخصصة"
|
||||||
|
_condition:
|
||||||
|
isLocal: "مستخدم محلي"
|
||||||
|
isRemote: "مستخدم بعيد"
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "هذا البريد الإلكتروني مستخدم"
|
used: "هذا البريد الإلكتروني مستخدم"
|
||||||
format: "صيغة البريد الإلكتروني غير صالحة"
|
format: "صيغة البريد الإلكتروني غير صالحة"
|
||||||
@ -1064,6 +1186,7 @@ _widgets:
|
|||||||
onlineUsers: "المتّصلون"
|
onlineUsers: "المتّصلون"
|
||||||
jobQueue: "قائمة الانتظار"
|
jobQueue: "قائمة الانتظار"
|
||||||
serverMetric: "إحصائيات الخادم"
|
serverMetric: "إحصائيات الخادم"
|
||||||
|
userList: "قائمة المستخدمين"
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "اختر قائمة"
|
chooseList: "اختر قائمة"
|
||||||
_cw:
|
_cw:
|
||||||
@ -1127,6 +1250,7 @@ _profile:
|
|||||||
changeBanner: "غيّر اللافتة"
|
changeBanner: "غيّر اللافتة"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "كل الملاحظات"
|
allNotes: "كل الملاحظات"
|
||||||
|
favoritedNotes: " الملاحظات المفضلة"
|
||||||
followingList: "المتابَعون"
|
followingList: "المتابَعون"
|
||||||
muteList: "المستخدمون المكتومون"
|
muteList: "المستخدمون المكتومون"
|
||||||
blockingList: "المستخدمون المحجوبون"
|
blockingList: "المستخدمون المحجوبون"
|
||||||
@ -1145,6 +1269,8 @@ _charts:
|
|||||||
notesTotal: "إجمالي الملاحظات"
|
notesTotal: "إجمالي الملاحظات"
|
||||||
filesIncDec: "تباين عدد الملفات"
|
filesIncDec: "تباين عدد الملفات"
|
||||||
filesTotal: "العدد الإجمالي للملفات"
|
filesTotal: "العدد الإجمالي للملفات"
|
||||||
|
storageUsageIncDec: "التباين في استغلال مساحة التخزين"
|
||||||
|
storageUsageTotal: "اجمالي مساحة التخزين المستغلة"
|
||||||
_instanceCharts:
|
_instanceCharts:
|
||||||
requests: "الطلبات"
|
requests: "الطلبات"
|
||||||
users: "تباين عدد المستخدمين"
|
users: "تباين عدد المستخدمين"
|
||||||
@ -1205,7 +1331,7 @@ _pages:
|
|||||||
text: "نص"
|
text: "نص"
|
||||||
textarea: "حقل نصي"
|
textarea: "حقل نصي"
|
||||||
section: "قسم"
|
section: "قسم"
|
||||||
image: "الصور"
|
image: "صور"
|
||||||
button: "زرّ"
|
button: "زرّ"
|
||||||
note: "ملاحظة مضمّنة"
|
note: "ملاحظة مضمّنة"
|
||||||
_note:
|
_note:
|
||||||
@ -1264,3 +1390,5 @@ _deck:
|
|||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "الإسم"
|
name: "الإسم"
|
||||||
active: "مفعّل"
|
active: "مفعّل"
|
||||||
|
_events:
|
||||||
|
reaction: "عند تلقي تفاعل"
|
||||||
|
@ -52,6 +52,8 @@ addToList: "Zu Liste hinzufügen"
|
|||||||
sendMessage: "Nachricht senden"
|
sendMessage: "Nachricht senden"
|
||||||
copyRSS: "RSS kopieren"
|
copyRSS: "RSS kopieren"
|
||||||
copyUsername: "Benutzernamen kopieren"
|
copyUsername: "Benutzernamen kopieren"
|
||||||
|
copyUserId: "Benutzer-ID kopieren"
|
||||||
|
copyNoteId: "Notiz-ID kopieren"
|
||||||
searchUser: "Nach einem Benutzer suchen"
|
searchUser: "Nach einem Benutzer suchen"
|
||||||
reply: "Antworten"
|
reply: "Antworten"
|
||||||
loadMore: "Mehr laden"
|
loadMore: "Mehr laden"
|
||||||
@ -790,6 +792,7 @@ noMaintainerInformationWarning: "Betreiberinformationen sind nicht konfiguriert.
|
|||||||
noBotProtectionWarning: "Schutz vor Bots ist nicht konfiguriert."
|
noBotProtectionWarning: "Schutz vor Bots ist nicht konfiguriert."
|
||||||
configure: "Konfigurieren"
|
configure: "Konfigurieren"
|
||||||
postToGallery: "Neuen Galeriebeitrag erstellen"
|
postToGallery: "Neuen Galeriebeitrag erstellen"
|
||||||
|
postToHashtag: "Mit diesem Hashtag senden"
|
||||||
gallery: "Galerie"
|
gallery: "Galerie"
|
||||||
recentPosts: "Neue Beiträge"
|
recentPosts: "Neue Beiträge"
|
||||||
popularPosts: "Beliebte Beiträge"
|
popularPosts: "Beliebte Beiträge"
|
||||||
@ -823,6 +826,7 @@ translatedFrom: "Aus {x} übersetzt"
|
|||||||
accountDeletionInProgress: "Die Löschung deines Benutzerkontos ist momentan in Bearbeitung."
|
accountDeletionInProgress: "Die Löschung deines Benutzerkontos ist momentan in Bearbeitung."
|
||||||
usernameInfo: "Ein Name, durch den dein Benutzerkonto auf diesem Server identifiziert werden kann. Du kannst das Alphabet (a~z, A~Z), Ziffern (0~9) oder Unterstriche (_) verwenden. Benutzernamen können später nicht geändert werden."
|
usernameInfo: "Ein Name, durch den dein Benutzerkonto auf diesem Server identifiziert werden kann. Du kannst das Alphabet (a~z, A~Z), Ziffern (0~9) oder Unterstriche (_) verwenden. Benutzernamen können später nicht geändert werden."
|
||||||
aiChanMode: "Ai-Modus"
|
aiChanMode: "Ai-Modus"
|
||||||
|
devMode: "Entwicklermodus"
|
||||||
keepCw: "Inhaltswarnungen beibehalten"
|
keepCw: "Inhaltswarnungen beibehalten"
|
||||||
pubSub: "Pub/Sub Benutzerkonten"
|
pubSub: "Pub/Sub Benutzerkonten"
|
||||||
lastCommunication: "Letzte Kommunikation"
|
lastCommunication: "Letzte Kommunikation"
|
||||||
@ -832,6 +836,8 @@ breakFollow: "Follower entfernen"
|
|||||||
breakFollowConfirm: "Diesen Follower wirklich entfernen?"
|
breakFollowConfirm: "Diesen Follower wirklich entfernen?"
|
||||||
itsOn: "Eingeschaltet"
|
itsOn: "Eingeschaltet"
|
||||||
itsOff: "Ausgeschaltet"
|
itsOff: "Ausgeschaltet"
|
||||||
|
on: "An"
|
||||||
|
off: "Aus"
|
||||||
emailRequiredForSignup: "Angabe einer Email-Adresse als benötigt markieren"
|
emailRequiredForSignup: "Angabe einer Email-Adresse als benötigt markieren"
|
||||||
unread: "Ungelesen"
|
unread: "Ungelesen"
|
||||||
filter: "Filter"
|
filter: "Filter"
|
||||||
@ -986,6 +992,8 @@ cannotBeChangedLater: "Kann später nicht mehr geändert werden."
|
|||||||
reactionAcceptance: "Reaktionsannahme"
|
reactionAcceptance: "Reaktionsannahme"
|
||||||
likeOnly: "Nur \"Gefällt mir\""
|
likeOnly: "Nur \"Gefällt mir\""
|
||||||
likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen"
|
likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen"
|
||||||
|
nonSensitiveOnly: "Keine Sensitiven"
|
||||||
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)"
|
||||||
rolesAssignedToMe: "Mir zugewiesene Rollen"
|
rolesAssignedToMe: "Mir zugewiesene Rollen"
|
||||||
resetPasswordConfirm: "Wirklich Passwort zurücksetzen?"
|
resetPasswordConfirm: "Wirklich Passwort zurücksetzen?"
|
||||||
sensitiveWords: "Sensible Wörter"
|
sensitiveWords: "Sensible Wörter"
|
||||||
@ -1043,6 +1051,17 @@ preventAiLearning: "Verwendung in machinellem Lernen (Generative bzw. Prediktive
|
|||||||
preventAiLearningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (Generative bzw. Prediktive AI/KI) zu verwenden. Dies wird durch das Hinzufügen einer \"noai\"-Flag in der HTML-Antwort des jeweiligen Inhalts erreicht. Da diese Flag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich."
|
preventAiLearningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (Generative bzw. Prediktive AI/KI) zu verwenden. Dies wird durch das Hinzufügen einer \"noai\"-Flag in der HTML-Antwort des jeweiligen Inhalts erreicht. Da diese Flag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich."
|
||||||
options: "Optionen"
|
options: "Optionen"
|
||||||
specifyUser: "Spezifischer Benutzer"
|
specifyUser: "Spezifischer Benutzer"
|
||||||
|
failedToPreviewUrl: "Vorschau nicht anzeigbar"
|
||||||
|
update: "Aktualisieren"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReaction: "Rollen, die dieses Emoji als Reaktion verwenden können"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Sind keine Rollen angegeben, kann jeder dieses Emoji als Reaktion verwenden."
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Diese Rollen müssen öffentlich sein."
|
||||||
|
cancelReactionConfirm: "Möchtest du deine Reaktion wirklich löschen?"
|
||||||
|
changeReactionConfirm: "Möchtest du deine Reaktion wirklich ändern?"
|
||||||
|
later: "Später"
|
||||||
|
goToMisskey: "Zu Misskey"
|
||||||
|
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
|
||||||
|
installed: "Installiert"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
|
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
|
||||||
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
|
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
|
||||||
@ -1057,6 +1076,7 @@ _initialAccountSetting:
|
|||||||
haveFun: "Viel Spaß mit {name}!"
|
haveFun: "Viel Spaß mit {name}!"
|
||||||
ifYouNeedLearnMore: "Besuche {link}, falls du mehr über {name} (Misskey) lernen möchtest."
|
ifYouNeedLearnMore: "Besuche {link}, falls du mehr über {name} (Misskey) lernen möchtest."
|
||||||
skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?"
|
skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?"
|
||||||
|
laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?"
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen."
|
description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
|
@ -52,6 +52,8 @@ addToList: "Add to list"
|
|||||||
sendMessage: "Send a message"
|
sendMessage: "Send a message"
|
||||||
copyRSS: "Copy RSS"
|
copyRSS: "Copy RSS"
|
||||||
copyUsername: "Copy username"
|
copyUsername: "Copy username"
|
||||||
|
copyUserId: "Copy user ID"
|
||||||
|
copyNoteId: "Copy note ID"
|
||||||
searchUser: "Search for a user"
|
searchUser: "Search for a user"
|
||||||
reply: "Reply"
|
reply: "Reply"
|
||||||
loadMore: "Load more"
|
loadMore: "Load more"
|
||||||
@ -790,6 +792,7 @@ noMaintainerInformationWarning: "Maintainer information is not configured."
|
|||||||
noBotProtectionWarning: "Bot protection is not configured."
|
noBotProtectionWarning: "Bot protection is not configured."
|
||||||
configure: "Configure"
|
configure: "Configure"
|
||||||
postToGallery: "Create new gallery post"
|
postToGallery: "Create new gallery post"
|
||||||
|
postToHashtag: "Post to this hashtag"
|
||||||
gallery: "Gallery"
|
gallery: "Gallery"
|
||||||
recentPosts: "Recent posts"
|
recentPosts: "Recent posts"
|
||||||
popularPosts: "Popular posts"
|
popularPosts: "Popular posts"
|
||||||
@ -823,6 +826,7 @@ translatedFrom: "Translated from {x}"
|
|||||||
accountDeletionInProgress: "Account deletion is currently in progress"
|
accountDeletionInProgress: "Account deletion is currently in progress"
|
||||||
usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
|
usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
|
||||||
aiChanMode: "Ai Mode"
|
aiChanMode: "Ai Mode"
|
||||||
|
devMode: "Developer mode"
|
||||||
keepCw: "Keep content warnings"
|
keepCw: "Keep content warnings"
|
||||||
pubSub: "Pub/Sub Accounts"
|
pubSub: "Pub/Sub Accounts"
|
||||||
lastCommunication: "Last communication"
|
lastCommunication: "Last communication"
|
||||||
@ -832,6 +836,8 @@ breakFollow: "Remove follower"
|
|||||||
breakFollowConfirm: "Really remove this follower?"
|
breakFollowConfirm: "Really remove this follower?"
|
||||||
itsOn: "Enabled"
|
itsOn: "Enabled"
|
||||||
itsOff: "Disabled"
|
itsOff: "Disabled"
|
||||||
|
on: "On"
|
||||||
|
off: "Off"
|
||||||
emailRequiredForSignup: "Require email address for sign-up"
|
emailRequiredForSignup: "Require email address for sign-up"
|
||||||
unread: "Unread"
|
unread: "Unread"
|
||||||
filter: "Filter"
|
filter: "Filter"
|
||||||
@ -986,6 +992,8 @@ cannotBeChangedLater: "This cannot be changed later."
|
|||||||
reactionAcceptance: "Reaction Acceptance"
|
reactionAcceptance: "Reaction Acceptance"
|
||||||
likeOnly: "Only likes"
|
likeOnly: "Only likes"
|
||||||
likeOnlyForRemote: "Only likes for remote instances"
|
likeOnlyForRemote: "Only likes for remote instances"
|
||||||
|
nonSensitiveOnly: "Non-sensitive only"
|
||||||
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)"
|
||||||
rolesAssignedToMe: "Roles assigned to me"
|
rolesAssignedToMe: "Roles assigned to me"
|
||||||
resetPasswordConfirm: "Really reset your password?"
|
resetPasswordConfirm: "Really reset your password?"
|
||||||
sensitiveWords: "Sensitive words"
|
sensitiveWords: "Sensitive words"
|
||||||
@ -1043,6 +1051,17 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)"
|
|||||||
preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
|
preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
|
||||||
options: "Options"
|
options: "Options"
|
||||||
specifyUser: "Specific user"
|
specifyUser: "Specific user"
|
||||||
|
failedToPreviewUrl: "Could not preview"
|
||||||
|
update: "Update"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "If no roles are specified, anyone can use this emoji as reaction."
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "These roles must be public."
|
||||||
|
cancelReactionConfirm: "Really delete your reaction?"
|
||||||
|
changeReactionConfirm: "Really change your reaction?"
|
||||||
|
later: "Later"
|
||||||
|
goToMisskey: "To Misskey"
|
||||||
|
additionalEmojiDictionary: "Additional emoji dictionaries"
|
||||||
|
installed: "Installed"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "Your account was successfully created!"
|
accountCreated: "Your account was successfully created!"
|
||||||
letsStartAccountSetup: "For starters, let's set up your profile."
|
letsStartAccountSetup: "For starters, let's set up your profile."
|
||||||
@ -1057,6 +1076,7 @@ _initialAccountSetting:
|
|||||||
haveFun: "Enjoy {name}!"
|
haveFun: "Enjoy {name}!"
|
||||||
ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}."
|
ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}."
|
||||||
skipAreYouSure: "Really skip profile setup?"
|
skipAreYouSure: "Really skip profile setup?"
|
||||||
|
laterAreYouSure: "Really do profile setup later?"
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended."
|
description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
|
@ -20,6 +20,7 @@ noNotes: "No hay notas"
|
|||||||
noNotifications: "No hay notificaciones"
|
noNotifications: "No hay notificaciones"
|
||||||
instance: "Instancia"
|
instance: "Instancia"
|
||||||
settings: "Configuración"
|
settings: "Configuración"
|
||||||
|
notificationSettings: "Configurar las notificaciones"
|
||||||
basicSettings: "Configuración Básica"
|
basicSettings: "Configuración Básica"
|
||||||
otherSettings: "Configuración avanzada"
|
otherSettings: "Configuración avanzada"
|
||||||
openInWindow: "Abrir en una ventana"
|
openInWindow: "Abrir en una ventana"
|
||||||
@ -262,8 +263,10 @@ noMoreHistory: "El historial se ha acabado"
|
|||||||
startMessaging: "Iniciar chat"
|
startMessaging: "Iniciar chat"
|
||||||
nUsersRead: "Leído por {n} personas"
|
nUsersRead: "Leído por {n} personas"
|
||||||
agreeTo: "De acuerdo con {0}"
|
agreeTo: "De acuerdo con {0}"
|
||||||
|
agree: "De acuerdo."
|
||||||
agreeBelow: "Estoy de acuerdo con lo siguiente"
|
agreeBelow: "Estoy de acuerdo con lo siguiente"
|
||||||
basicNotesBeforeCreateAccount: "Notas básicas"
|
basicNotesBeforeCreateAccount: "Notas básicas"
|
||||||
|
termsOfService: "Términos y condiciones"
|
||||||
start: "Comenzar"
|
start: "Comenzar"
|
||||||
home: "Inicio"
|
home: "Inicio"
|
||||||
remoteUserCaution: "Para el usuario remoto, la información está incompleta"
|
remoteUserCaution: "Para el usuario remoto, la información está incompleta"
|
||||||
@ -473,6 +476,8 @@ createAccount: "Crear cuenta"
|
|||||||
existingAccount: "Cuenta existente"
|
existingAccount: "Cuenta existente"
|
||||||
regenerate: "Regenerar"
|
regenerate: "Regenerar"
|
||||||
fontSize: "Tamaño de la letra"
|
fontSize: "Tamaño de la letra"
|
||||||
|
mediaListWithOneImageAppearance: "Altura de la lista de medios con una sola imagen."
|
||||||
|
limitTo: "{x} hasta un máximo de"
|
||||||
noFollowRequests: "No hay solicitudes de seguimiento"
|
noFollowRequests: "No hay solicitudes de seguimiento"
|
||||||
openImageInNewTab: "Abrir imagen en nueva pestaña"
|
openImageInNewTab: "Abrir imagen en nueva pestaña"
|
||||||
dashboard: "Panel de control"
|
dashboard: "Panel de control"
|
||||||
@ -555,6 +560,7 @@ accountDeletedDescription: "Esta cuenta ha sido borrada."
|
|||||||
menu: "Menú"
|
menu: "Menú"
|
||||||
divider: "Divisor"
|
divider: "Divisor"
|
||||||
addItem: "Agregar elemento"
|
addItem: "Agregar elemento"
|
||||||
|
rearrange: "Ordenar"
|
||||||
relays: "Relés"
|
relays: "Relés"
|
||||||
addRelay: "Agregar relé"
|
addRelay: "Agregar relé"
|
||||||
inboxUrl: "Inbox URL"
|
inboxUrl: "Inbox URL"
|
||||||
@ -698,6 +704,8 @@ contact: "Contacto"
|
|||||||
useSystemFont: "Utilizar la tipografía por defecto del sistema"
|
useSystemFont: "Utilizar la tipografía por defecto del sistema"
|
||||||
clips: "Clip"
|
clips: "Clip"
|
||||||
experimentalFeatures: "Características experimentales"
|
experimentalFeatures: "Características experimentales"
|
||||||
|
experimental: "Función experimental"
|
||||||
|
thisIsExperimentalFeature: "Se trata de una función experimental. Las especificaciones pueden cambiar o puede que no funcione correctamente."
|
||||||
developer: "Desarrolladores"
|
developer: "Desarrolladores"
|
||||||
makeExplorable: "Hacer visible la cuenta en \"Explorar\""
|
makeExplorable: "Hacer visible la cuenta en \"Explorar\""
|
||||||
makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la sección \"Explorar\"."
|
makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la sección \"Explorar\"."
|
||||||
@ -991,9 +999,12 @@ largeNoteReactions: "Agrandar las reacciones de las notas"
|
|||||||
noteIdOrUrl: "ID o URL de la nota"
|
noteIdOrUrl: "ID o URL de la nota"
|
||||||
accountMigration: "Migración de cuenta"
|
accountMigration: "Migración de cuenta"
|
||||||
accountMoved: "Este usuario se ha mudado a una nueva cuenta:"
|
accountMoved: "Este usuario se ha mudado a una nueva cuenta:"
|
||||||
|
accountMovedShort: "Esta cuenta ha sido migrada."
|
||||||
horizontal: "Horizontal"
|
horizontal: "Horizontal"
|
||||||
youFollowing: "Siguiendo"
|
youFollowing: "Siguiendo"
|
||||||
options: "Opción"
|
options: "Opción"
|
||||||
|
_initialAccountSetting:
|
||||||
|
accountCreated: "¡La cuenta ha sido creada!"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "Trasladar de otra cuenta a ésta"
|
moveFrom: "Trasladar de otra cuenta a ésta"
|
||||||
moveFromLabel: "Cuenta desde la que se realiza el traslado:"
|
moveFromLabel: "Cuenta desde la que se realiza el traslado:"
|
||||||
@ -1174,6 +1185,8 @@ _achievements:
|
|||||||
_client30min:
|
_client30min:
|
||||||
title: "Un descansito"
|
title: "Un descansito"
|
||||||
description: "30 minutos dedicados a Misskey"
|
description: "30 minutos dedicados a Misskey"
|
||||||
|
_client60min:
|
||||||
|
title: "Viendo mucho Misskey."
|
||||||
_noteDeletedWithin1min:
|
_noteDeletedWithin1min:
|
||||||
title: "Ah... Mejor no..."
|
title: "Ah... Mejor no..."
|
||||||
description: "Borrar una nota antes que de pase 1 minuto"
|
description: "Borrar una nota antes que de pase 1 minuto"
|
||||||
|
@ -908,12 +908,14 @@ neverShow: "Ne plus afficher"
|
|||||||
remindMeLater: "Peut-être plus tard"
|
remindMeLater: "Peut-être plus tard"
|
||||||
roles: "Rôles"
|
roles: "Rôles"
|
||||||
role: "Rôles"
|
role: "Rôles"
|
||||||
|
noRole: "Aucun rôle"
|
||||||
normalUser: "Simple utilisateur·rice"
|
normalUser: "Simple utilisateur·rice"
|
||||||
assign: "Attribuer"
|
assign: "Attribuer"
|
||||||
color: "Couleur"
|
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"
|
||||||
|
thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes."
|
||||||
thisPostMayBeAnnoyingCancel: "Annuler"
|
thisPostMayBeAnnoyingCancel: "Annuler"
|
||||||
license: "Licence"
|
license: "Licence"
|
||||||
video: "Vidéo"
|
video: "Vidéo"
|
||||||
@ -921,6 +923,7 @@ videos: "Vidéos"
|
|||||||
dataSaver: "Économiseur de données"
|
dataSaver: "Économiseur de données"
|
||||||
accountMigration: "Migration de compte"
|
accountMigration: "Migration de compte"
|
||||||
accountMoved: "Cet·te utilisateur·rice a migré son compte vers :"
|
accountMoved: "Cet·te utilisateur·rice a migré son compte vers :"
|
||||||
|
addMemo: "Ajouter un mémo"
|
||||||
notificationDisplay: "Style des notifications"
|
notificationDisplay: "Style des notifications"
|
||||||
leftTop: "En haut à gauche"
|
leftTop: "En haut à gauche"
|
||||||
rightTop: "En haut à droite"
|
rightTop: "En haut à droite"
|
||||||
@ -935,6 +938,8 @@ _achievements:
|
|||||||
_notes1:
|
_notes1:
|
||||||
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 !"
|
||||||
|
_notes100:
|
||||||
|
title: "Beaucoup de notes"
|
||||||
_notes100000:
|
_notes100000:
|
||||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||||
_login3:
|
_login3:
|
||||||
@ -985,6 +990,8 @@ _achievements:
|
|||||||
title: "Joyeux Anniversaire !"
|
title: "Joyeux Anniversaire !"
|
||||||
_loggedInOnNewYearsDay:
|
_loggedInOnNewYearsDay:
|
||||||
title: "Bonne année !"
|
title: "Bonne année !"
|
||||||
|
_cookieClicked:
|
||||||
|
flavor: "Attendez une minute, vous êtes sur le mauvais site web ?"
|
||||||
_role:
|
_role:
|
||||||
assignTarget: "Attribuer"
|
assignTarget: "Attribuer"
|
||||||
priority: "Priorité"
|
priority: "Priorité"
|
||||||
|
72
locales/generateDTS.js
Normal file
72
locales/generateDTS.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const ts = require('typescript');
|
||||||
|
|
||||||
|
function createMembers(record) {
|
||||||
|
return Object.entries(record)
|
||||||
|
.map(([k, v]) => ts.factory.createPropertySignature(
|
||||||
|
undefined,
|
||||||
|
ts.factory.createStringLiteral(k),
|
||||||
|
undefined,
|
||||||
|
typeof v === 'string'
|
||||||
|
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
|
||||||
|
: ts.factory.createTypeLiteralNode(createMembers(v)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function generateDTS() {
|
||||||
|
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
|
||||||
|
const members = createMembers(locale);
|
||||||
|
const elements = [
|
||||||
|
ts.factory.createInterfaceDeclaration(
|
||||||
|
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||||
|
ts.factory.createIdentifier('Locale'),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
members,
|
||||||
|
),
|
||||||
|
ts.factory.createVariableStatement(
|
||||||
|
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
|
||||||
|
ts.factory.createVariableDeclarationList(
|
||||||
|
[ts.factory.createVariableDeclaration(
|
||||||
|
ts.factory.createIdentifier('locales'),
|
||||||
|
undefined,
|
||||||
|
ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature(
|
||||||
|
undefined,
|
||||||
|
[ts.factory.createParameterDeclaration(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
ts.factory.createIdentifier('lang'),
|
||||||
|
undefined,
|
||||||
|
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||||
|
undefined,
|
||||||
|
)],
|
||||||
|
ts.factory.createTypeReferenceNode(
|
||||||
|
ts.factory.createIdentifier('Locale'),
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
)]),
|
||||||
|
undefined,
|
||||||
|
)],
|
||||||
|
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ts.factory.createExportAssignment(
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
ts.factory.createIdentifier('locales'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const printed = ts.createPrinter({
|
||||||
|
newLine: ts.NewLineKind.LineFeed,
|
||||||
|
}).printList(
|
||||||
|
ts.ListFormat.MultiLine,
|
||||||
|
ts.factory.createNodeArray(elements),
|
||||||
|
ts.createSourceFile('index.d.ts', '', ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS),
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(`${__dirname}/index.d.ts`, `/* eslint-disable */
|
||||||
|
// This file is generated by locales/generateDTS.js
|
||||||
|
// Do not edit this file directly.
|
||||||
|
${printed}`, 'utf-8');
|
||||||
|
}
|
@ -20,6 +20,7 @@ noNotes: "Tidak ada catatan"
|
|||||||
noNotifications: "Tidak ada pemberitahuan"
|
noNotifications: "Tidak ada pemberitahuan"
|
||||||
instance: "Instansi"
|
instance: "Instansi"
|
||||||
settings: "Pengaturan"
|
settings: "Pengaturan"
|
||||||
|
notificationSettings: "Atur Notifikasi"
|
||||||
basicSettings: "Pengaturan umum"
|
basicSettings: "Pengaturan umum"
|
||||||
otherSettings: "Pengaturan lainnya"
|
otherSettings: "Pengaturan lainnya"
|
||||||
openInWindow: "Buka di jendela"
|
openInWindow: "Buka di jendela"
|
||||||
@ -261,8 +262,10 @@ noMoreHistory: "Tidak ada sejarah lagi"
|
|||||||
startMessaging: "Mulai mengirim pesan"
|
startMessaging: "Mulai mengirim pesan"
|
||||||
nUsersRead: "Dibaca oleh {n}"
|
nUsersRead: "Dibaca oleh {n}"
|
||||||
agreeTo: "Saya setuju kepada {0}"
|
agreeTo: "Saya setuju kepada {0}"
|
||||||
|
agree: "Setuju"
|
||||||
agreeBelow: "Saya setuju dengan di bawah ini"
|
agreeBelow: "Saya setuju dengan di bawah ini"
|
||||||
basicNotesBeforeCreateAccount: "Catatan penting"
|
basicNotesBeforeCreateAccount: "Catatan penting"
|
||||||
|
termsOfService: "Syarat dan ketentuan"
|
||||||
start: "Mulai"
|
start: "Mulai"
|
||||||
home: "Beranda"
|
home: "Beranda"
|
||||||
remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari instansi luar."
|
remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari instansi luar."
|
||||||
|
2151
locales/index.d.ts
vendored
2151
locales/index.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -52,6 +52,8 @@ addToList: "リストに追加"
|
|||||||
sendMessage: "メッセージを送信"
|
sendMessage: "メッセージを送信"
|
||||||
copyRSS: "RSSをコピー"
|
copyRSS: "RSSをコピー"
|
||||||
copyUsername: "ユーザー名をコピー"
|
copyUsername: "ユーザー名をコピー"
|
||||||
|
copyUserId: "ユーザーIDをコピー"
|
||||||
|
copyNoteId: "ノートIDをコピー"
|
||||||
searchUser: "ユーザーを検索"
|
searchUser: "ユーザーを検索"
|
||||||
reply: "返信"
|
reply: "返信"
|
||||||
loadMore: "もっと見る"
|
loadMore: "もっと見る"
|
||||||
@ -790,6 +792,7 @@ noMaintainerInformationWarning: "管理者情報が設定されていません
|
|||||||
noBotProtectionWarning: "Botプロテクションが設定されていません。"
|
noBotProtectionWarning: "Botプロテクションが設定されていません。"
|
||||||
configure: "設定する"
|
configure: "設定する"
|
||||||
postToGallery: "ギャラリーへ投稿"
|
postToGallery: "ギャラリーへ投稿"
|
||||||
|
postToHashtag: "このハッシュタグで投稿"
|
||||||
gallery: "ギャラリー"
|
gallery: "ギャラリー"
|
||||||
recentPosts: "最近の投稿"
|
recentPosts: "最近の投稿"
|
||||||
popularPosts: "人気の投稿"
|
popularPosts: "人気の投稿"
|
||||||
@ -823,6 +826,7 @@ translatedFrom: "{x}から翻訳"
|
|||||||
accountDeletionInProgress: "アカウントの削除が進行中です"
|
accountDeletionInProgress: "アカウントの削除が進行中です"
|
||||||
usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。"
|
usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。"
|
||||||
aiChanMode: "藍モード"
|
aiChanMode: "藍モード"
|
||||||
|
devMode: "開発者モード"
|
||||||
keepCw: "CWを維持する"
|
keepCw: "CWを維持する"
|
||||||
pubSub: "Pub/Subのアカウント"
|
pubSub: "Pub/Subのアカウント"
|
||||||
lastCommunication: "直近の通信"
|
lastCommunication: "直近の通信"
|
||||||
@ -987,7 +991,9 @@ postToTheChannel: "チャンネルに投稿"
|
|||||||
cannotBeChangedLater: "後から変更できません。"
|
cannotBeChangedLater: "後から変更できません。"
|
||||||
reactionAcceptance: "リアクションの受け入れ"
|
reactionAcceptance: "リアクションの受け入れ"
|
||||||
likeOnly: "いいねのみ"
|
likeOnly: "いいねのみ"
|
||||||
likeOnlyForRemote: "リモートからはいいねのみ"
|
likeOnlyForRemote: "全て (リモートはいいねのみ)"
|
||||||
|
nonSensitiveOnly: "非センシティブのみ"
|
||||||
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "非センシティブのみ (リモートはいいねのみ)"
|
||||||
rolesAssignedToMe: "自分に割り当てられたロール"
|
rolesAssignedToMe: "自分に割り当てられたロール"
|
||||||
resetPasswordConfirm: "パスワードリセットしますか?"
|
resetPasswordConfirm: "パスワードリセットしますか?"
|
||||||
sensitiveWords: "センシティブワード"
|
sensitiveWords: "センシティブワード"
|
||||||
@ -1045,6 +1051,17 @@ preventAiLearning: "生成AIによる学習を拒否"
|
|||||||
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
|
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
|
||||||
options: "オプション"
|
options: "オプション"
|
||||||
specifyUser: "ユーザー指定"
|
specifyUser: "ユーザー指定"
|
||||||
|
failedToPreviewUrl: "プレビューできません"
|
||||||
|
update: "更新"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールの指定が一つもない場合、誰でもリアクションとして使えます。"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールである必要があります。"
|
||||||
|
cancelReactionConfirm: "リアクションを取り消しますか?"
|
||||||
|
changeReactionConfirm: "リアクションを変更しますか?"
|
||||||
|
later: "あとで"
|
||||||
|
goToMisskey: "Misskeyへ"
|
||||||
|
additionalEmojiDictionary: "絵文字の追加辞書"
|
||||||
|
installed: "インストール済み"
|
||||||
|
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "アカウントの作成が完了しました!"
|
accountCreated: "アカウントの作成が完了しました!"
|
||||||
@ -1060,6 +1077,7 @@ _initialAccountSetting:
|
|||||||
haveFun: "{name}をお楽しみください!"
|
haveFun: "{name}をお楽しみください!"
|
||||||
ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。"
|
ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。"
|
||||||
skipAreYouSure: "初期設定をスキップしますか?"
|
skipAreYouSure: "初期設定をスキップしますか?"
|
||||||
|
laterAreYouSure: "初期設定をあとでやり直しますか?"
|
||||||
|
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||||
|
@ -8,7 +8,7 @@ search: "探す"
|
|||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
username: "ユーザー名"
|
username: "ユーザー名"
|
||||||
password: "パスワード"
|
password: "パスワード"
|
||||||
forgotPassword: "パスワード忘れてもうた"
|
forgotPassword: "パスワード忘れたん?"
|
||||||
fetchingAsApObject: "今ちと連合に照会しとるで"
|
fetchingAsApObject: "今ちと連合に照会しとるで"
|
||||||
ok: "ええで"
|
ok: "ええで"
|
||||||
gotIt: "ほい"
|
gotIt: "ほい"
|
||||||
@ -47,11 +47,13 @@ copyContent: "内容をコピー"
|
|||||||
copyLink: "リンクをコピー"
|
copyLink: "リンクをコピー"
|
||||||
delete: "ほかす"
|
delete: "ほかす"
|
||||||
deleteAndEdit: "ほかして直す"
|
deleteAndEdit: "ほかして直す"
|
||||||
deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのリアクション、Renote、返信も全部消えるんやけどそれでもええん?"
|
deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん?"
|
||||||
addToList: "リストに入れたる"
|
addToList: "リストに入れたる"
|
||||||
sendMessage: "メッセージを送る"
|
sendMessage: "メッセージを送る"
|
||||||
copyRSS: "RSSをコピー"
|
copyRSS: "RSSをコピー"
|
||||||
copyUsername: "ユーザー名をコピー"
|
copyUsername: "ユーザー名をコピー"
|
||||||
|
copyUserId: "ユーザーIDをコピー"
|
||||||
|
copyNoteId: "ノートIDをコピー"
|
||||||
searchUser: "ユーザーを検索"
|
searchUser: "ユーザーを検索"
|
||||||
reply: "返事"
|
reply: "返事"
|
||||||
loadMore: "まだまだあるで!"
|
loadMore: "まだまだあるで!"
|
||||||
@ -790,6 +792,7 @@ noMaintainerInformationWarning: "管理者情報が設定されてへんで"
|
|||||||
noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
|
noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
|
||||||
configure: "設定する"
|
configure: "設定する"
|
||||||
postToGallery: "ギャラリーへ投稿"
|
postToGallery: "ギャラリーへ投稿"
|
||||||
|
postToHashtag: "このハッシュタグで投稿"
|
||||||
gallery: "ギャラリー"
|
gallery: "ギャラリー"
|
||||||
recentPosts: "最近の投稿"
|
recentPosts: "最近の投稿"
|
||||||
popularPosts: "人気の投稿"
|
popularPosts: "人気の投稿"
|
||||||
@ -823,6 +826,7 @@ translatedFrom: "{x}から翻訳するで"
|
|||||||
accountDeletionInProgress: "アカウント削除しとるで待っとってなー"
|
accountDeletionInProgress: "アカウント削除しとるで待っとってなー"
|
||||||
usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。"
|
usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。"
|
||||||
aiChanMode: "藍モードやで"
|
aiChanMode: "藍モードやで"
|
||||||
|
devMode: "開発者モード"
|
||||||
keepCw: "CWを維持するで"
|
keepCw: "CWを維持するで"
|
||||||
pubSub: "Pub/Subのアカウント"
|
pubSub: "Pub/Subのアカウント"
|
||||||
lastCommunication: "直近の通信"
|
lastCommunication: "直近の通信"
|
||||||
@ -832,6 +836,8 @@ breakFollow: "フォロワーを解除するで"
|
|||||||
breakFollowConfirm: "フォロワー解除してもええか?"
|
breakFollowConfirm: "フォロワー解除してもええか?"
|
||||||
itsOn: "オンになっとるよ"
|
itsOn: "オンになっとるよ"
|
||||||
itsOff: "オフになってるで"
|
itsOff: "オフになってるで"
|
||||||
|
on: "オン"
|
||||||
|
off: "オフ"
|
||||||
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで"
|
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで"
|
||||||
unread: "未読"
|
unread: "未読"
|
||||||
filter: "フィルタ"
|
filter: "フィルタ"
|
||||||
@ -986,6 +992,8 @@ cannotBeChangedLater: "後からは変えられへんで。"
|
|||||||
reactionAcceptance: "ツッコミの受け入れ"
|
reactionAcceptance: "ツッコミの受け入れ"
|
||||||
likeOnly: "いいねだけ"
|
likeOnly: "いいねだけ"
|
||||||
likeOnlyForRemote: "リモートからはいいねだけな"
|
likeOnlyForRemote: "リモートからはいいねだけな"
|
||||||
|
nonSensitiveOnly: "センシティブじゃないやつだけ"
|
||||||
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "センシティブじゃないやつだけ (リモートはいいねだけ)"
|
||||||
rolesAssignedToMe: "自分に割り当てられたロール"
|
rolesAssignedToMe: "自分に割り当てられたロール"
|
||||||
resetPasswordConfirm: "パスワード作り直すんでええな?"
|
resetPasswordConfirm: "パスワード作り直すんでええな?"
|
||||||
sensitiveWords: "けったいな単語"
|
sensitiveWords: "けったいな単語"
|
||||||
@ -1043,6 +1051,17 @@ preventAiLearning: "生成AIの学習に使わんといて"
|
|||||||
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
|
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
|
||||||
options: "オプション"
|
options: "オプション"
|
||||||
specifyUser: "ユーザー指定"
|
specifyUser: "ユーザー指定"
|
||||||
|
failedToPreviewUrl: "プレビューできへん"
|
||||||
|
update: "更新"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールじゃないとアカンで。"
|
||||||
|
cancelReactionConfirm: "ツッコむんをやっぱやめるか?"
|
||||||
|
changeReactionConfirm: "ツッコミを別のに変えるか?"
|
||||||
|
later: "あとで"
|
||||||
|
goToMisskey: "Misskeyへ"
|
||||||
|
additionalEmojiDictionary: "絵文字の追加辞書"
|
||||||
|
installed: "インストール済み"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "アカウント作り終わったで。"
|
accountCreated: "アカウント作り終わったで。"
|
||||||
letsStartAccountSetup: "アカウントの初期設定をしよか。"
|
letsStartAccountSetup: "アカウントの初期設定をしよか。"
|
||||||
@ -1057,6 +1076,7 @@ _initialAccountSetting:
|
|||||||
haveFun: "{name}、楽しんでな~"
|
haveFun: "{name}、楽しんでな~"
|
||||||
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
|
ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。"
|
||||||
skipAreYouSure: "初期設定飛ばすか?"
|
skipAreYouSure: "初期設定飛ばすか?"
|
||||||
|
laterAreYouSure: "初期設定あとでやり直すん?"
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
|
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
@ -1614,7 +1634,7 @@ _timelineTutorial:
|
|||||||
step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。"
|
step2_2: "最初のノートは、自己紹介とか「{name}始めてみたんや」とかがええと思うで。"
|
||||||
step3_1: "投稿できた?"
|
step3_1: "投稿できた?"
|
||||||
step3_2: "あんたのノートがタイムラインに出てきたら成功や。"
|
step3_2: "あんたのノートがタイムラインに出てきたら成功や。"
|
||||||
step4_1: "ノートには、「リアクション」を付けれるで。"
|
step4_1: "ノートには、「ツッコミ」を付けれるで。"
|
||||||
step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。"
|
step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "もう設定終わっとるわ。"
|
alreadyRegistered: "もう設定終わっとるわ。"
|
||||||
|
@ -52,6 +52,8 @@ addToList: "리스트에 추가"
|
|||||||
sendMessage: "메시지 보내기"
|
sendMessage: "메시지 보내기"
|
||||||
copyRSS: "RSS 복사"
|
copyRSS: "RSS 복사"
|
||||||
copyUsername: "유저명 복사"
|
copyUsername: "유저명 복사"
|
||||||
|
copyUserId: "유저 ID 복사"
|
||||||
|
copyNoteId: "노트 ID 복사"
|
||||||
searchUser: "사용자 검색"
|
searchUser: "사용자 검색"
|
||||||
reply: "답글"
|
reply: "답글"
|
||||||
loadMore: "더 보기"
|
loadMore: "더 보기"
|
||||||
@ -505,7 +507,7 @@ objectStoragePrefixDesc: "이 Prefix 의 디렉토리 아래에 파일이 저장
|
|||||||
objectStorageEndpoint: "Endpoint"
|
objectStorageEndpoint: "Endpoint"
|
||||||
objectStorageEndpointDesc: "AWS S3의 경우 공란, 다른 서비스의 경우 각 서비스의 가이드에 맞게 endpoint를 설정해주세요. '<host>' 혹은 '<host>:<port>' 와 같이 지정합니다."
|
objectStorageEndpointDesc: "AWS S3의 경우 공란, 다른 서비스의 경우 각 서비스의 가이드에 맞게 endpoint를 설정해주세요. '<host>' 혹은 '<host>:<port>' 와 같이 지정합니다."
|
||||||
objectStorageRegion: "Region"
|
objectStorageRegion: "Region"
|
||||||
objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해주세요. 사용하는 서비스에 region 개념이 없는 경우, 비워 두거나 'us-east-1'으로 설정해 주세요."
|
objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해 주세요. 사용하는 서비스에 region 개념이 없는 경우 'us-east-1'으로 설정해 주세요. AWS 설정 파일 또는 환경 변수를 참조할 경우에는 비워주세요."
|
||||||
objectStorageUseSSL: "SSL 사용"
|
objectStorageUseSSL: "SSL 사용"
|
||||||
objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요"
|
objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요"
|
||||||
objectStorageUseProxy: "연결에 프록시를 사용"
|
objectStorageUseProxy: "연결에 프록시를 사용"
|
||||||
@ -790,6 +792,7 @@ noMaintainerInformationWarning: "관리자 정보가 설정되어 있지 않습
|
|||||||
noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다."
|
noBotProtectionWarning: "Bot 방어가 설정되어 있지 않습니다."
|
||||||
configure: "설정하기"
|
configure: "설정하기"
|
||||||
postToGallery: "갤러리에 업로드"
|
postToGallery: "갤러리에 업로드"
|
||||||
|
postToHashtag: "이 해시태그에 게시"
|
||||||
gallery: "갤러리"
|
gallery: "갤러리"
|
||||||
recentPosts: "최근 포스트"
|
recentPosts: "최근 포스트"
|
||||||
popularPosts: "인기 포스트"
|
popularPosts: "인기 포스트"
|
||||||
@ -823,6 +826,7 @@ translatedFrom: "{x}에서 번역"
|
|||||||
accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다"
|
accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다"
|
||||||
usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다."
|
usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다."
|
||||||
aiChanMode: "아이 모드"
|
aiChanMode: "아이 모드"
|
||||||
|
devMode: "개발자 모드"
|
||||||
keepCw: "CW 유지하기"
|
keepCw: "CW 유지하기"
|
||||||
pubSub: "Pub/Sub 계정"
|
pubSub: "Pub/Sub 계정"
|
||||||
lastCommunication: "마지막 통신"
|
lastCommunication: "마지막 통신"
|
||||||
@ -830,8 +834,10 @@ resolved: "해결됨"
|
|||||||
unresolved: "해결되지 않음"
|
unresolved: "해결되지 않음"
|
||||||
breakFollow: "팔로워 해제"
|
breakFollow: "팔로워 해제"
|
||||||
breakFollowConfirm: "팔로우를 해제하시겠습니까?"
|
breakFollowConfirm: "팔로우를 해제하시겠습니까?"
|
||||||
itsOn: "켜짐"
|
itsOn: "켜져 있습니다"
|
||||||
itsOff: "꺼짐"
|
itsOff: "꺼져 있습니다"
|
||||||
|
on: "켜짐"
|
||||||
|
off: "꺼짐"
|
||||||
emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기"
|
emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기"
|
||||||
unread: "읽지 않음"
|
unread: "읽지 않음"
|
||||||
filter: "필터"
|
filter: "필터"
|
||||||
@ -864,7 +870,7 @@ instanceDefaultLightTheme: "서버 기본 라이트 테마"
|
|||||||
instanceDefaultDarkTheme: "서버 기본 다크 테마"
|
instanceDefaultDarkTheme: "서버 기본 다크 테마"
|
||||||
instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요."
|
instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요."
|
||||||
mutePeriod: "뮤트할 기간"
|
mutePeriod: "뮤트할 기간"
|
||||||
period: "투표 기한"
|
period: "기간"
|
||||||
indefinitely: "무기한"
|
indefinitely: "무기한"
|
||||||
tenMinutes: "10분"
|
tenMinutes: "10분"
|
||||||
oneHour: "1시간"
|
oneHour: "1시간"
|
||||||
@ -986,10 +992,13 @@ cannotBeChangedLater: "나중에 변경할 수 없습니다."
|
|||||||
reactionAcceptance: "리액션 수신"
|
reactionAcceptance: "리액션 수신"
|
||||||
likeOnly: "좋아요만 받기"
|
likeOnly: "좋아요만 받기"
|
||||||
likeOnlyForRemote: "리모트에서는 좋아요만 받기"
|
likeOnlyForRemote: "리모트에서는 좋아요만 받기"
|
||||||
|
nonSensitiveOnly: "열람 주의로 설정되지 않았을 때만 받기"
|
||||||
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "열람 주의로 설정되지 않았을 때만 받기 (리모트에서는 좋아요만 받기)"
|
||||||
rolesAssignedToMe: "나에게 할당된 역할"
|
rolesAssignedToMe: "나에게 할당된 역할"
|
||||||
resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
|
resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
|
||||||
sensitiveWords: "민감한 단어"
|
sensitiveWords: "민감한 단어"
|
||||||
sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다."
|
sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다."
|
||||||
|
sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
|
||||||
notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다."
|
notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다."
|
||||||
license: "라이선스"
|
license: "라이선스"
|
||||||
unfavoriteConfirm: "즐겨찾기를 해제하시겠습니까?"
|
unfavoriteConfirm: "즐겨찾기를 해제하시겠습니까?"
|
||||||
@ -1038,31 +1047,47 @@ thisChannelArchived: "이 채널은 아카이브되었습니다."
|
|||||||
displayOfNote: "노트 표시"
|
displayOfNote: "노트 표시"
|
||||||
initialAccountSetting: "초기 설정"
|
initialAccountSetting: "초기 설정"
|
||||||
youFollowing: "팔로잉"
|
youFollowing: "팔로잉"
|
||||||
|
preventAiLearning: "기계학습(생성형 AI)으로의 사용을 거부"
|
||||||
|
preventAiLearningDescription: "외부의 문장 생성 AI나 이미지 생성 AI에 대해 제출한 노트나 이미지 등의 콘텐츠를 학습의 대상으로 사용하지 않도록 요구합니다. 다만, 이 요구사항을 지킬 의무는 없기 때문에 학습을 완전히 방지하는 것은 아닙니다."
|
||||||
options: "옵션"
|
options: "옵션"
|
||||||
|
specifyUser: "사용자 지정"
|
||||||
|
failedToPreviewUrl: "미리 볼 수 없음"
|
||||||
|
update: "업데이트"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReaction: "이 이모지를 리액션으로 사용할 수 있는 역할"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "역할을 지정하지 않으면, 누구나 이 이모지를 리액션으로 사용할 수 있습니다."
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "역할은 공개로 설정되어 있어야 합니다."
|
||||||
|
cancelReactionConfirm: "리액션을 취소하시겠습니까?"
|
||||||
|
changeReactionConfirm: "리액션을 변경하시겠습니까?"
|
||||||
|
later: "나중에"
|
||||||
|
goToMisskey: "Misskey로"
|
||||||
|
additionalEmojiDictionary: "이모지 추가 사전"
|
||||||
|
installed: "설치됨"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "계정 생성이 완료되었습니다!"
|
accountCreated: "계정 생성이 완료되었습니다!"
|
||||||
letsStartAccountSetup: "계정의 초기 설정을 진행합니다."
|
letsStartAccountSetup: "계정의 초기 설정을 진행합니다."
|
||||||
letsFillYourProfile: "우선 나의 프로필을 설정해 보아요."
|
letsFillYourProfile: "우선 나의 프로필을 설정해 보아요."
|
||||||
profileSetting: "프로필 설정"
|
profileSetting: "프로필 설정"
|
||||||
|
privacySetting: "프라이버시 설정"
|
||||||
theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있습니다."
|
theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있습니다."
|
||||||
youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 맛게 조절할 수 있습니다. 꼭 확인해 보세요!"
|
youCanEditMoreSettingsInSettingsPageLater: "이 외에도 '설정' 페이지에서 다양한 설정을 나의 입맛에 맞게 조절할 수 있습니다. 꼭 확인해 보세요!"
|
||||||
followUsers: "관심사가 맞는 유저를 팔로우하여 타임라인을 가꾸어 봅시다."
|
followUsers: "관심사가 맞는 유저를 팔로우하여 타임라인을 가꾸어 봅시다."
|
||||||
pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다."
|
pushNotificationDescription: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다."
|
||||||
initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!"
|
initialAccountSettingCompleted: "초기 설정을 모두 마쳤습니다!"
|
||||||
haveFun: "{name}와 함께 즐거운 시간 보내세요!"
|
haveFun: "{name}와 함께 즐거운 시간 보내세요!"
|
||||||
ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요."
|
ifYouNeedLearnMore: "{name}(Misskey)의 사용 방법에 대해 자세히 알아보려면 {link}를 참고해 주세요."
|
||||||
skipAreYouSure: "초기 설정을 넘기시겠습니까?"
|
skipAreYouSure: "초기 설정을 중단하시겠습니까?"
|
||||||
|
laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?"
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다."
|
description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "다른 계정에서 이 계정으로 이사"
|
moveFrom: "다른 계정에서 이 계정으로 이사"
|
||||||
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
||||||
moveFromLabel: "기존 계정:"
|
moveFromLabel: "기존 계정 #{n}"
|
||||||
moveFromDescription: "다른 계정에서 이 계정으로 팔로워를 가져오려면, 우선 여기에서 별칭을 지정해야 합니다. 반드시 이사하기 전에 지정해야 합니다! 기존 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com"
|
moveFromDescription: "다른 계정에서 이 계정으로 팔로워를 가져오려면, 우선 여기에서 별칭을 지정해야 합니다. 반드시 이사하기 전에 지정해야 합니다! 기존 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com"
|
||||||
moveTo: "이 계정에서 다른 계정으로 이사"
|
moveTo: "이 계정에서 다른 계정으로 이사"
|
||||||
moveToLabel: "이사할 계정:"
|
moveToLabel: "이사할 계정:"
|
||||||
moveCannotBeUndone: "한 번 이사하면, 두 번 다시 되돌릴 수 없습니다."
|
moveCannotBeUndone: "한 번 이사하면, 두 번 다시 되돌릴 수 없습니다."
|
||||||
moveAccountDescription: "이 작업은 취소할 수 없습니다. 먼저 이사할 계정에서 이 계정에 대한 별칭을 지정하였는지 다시 한 번 확인해 주십시오. 별칭을 지정한 다음, 이사할 계정을 다음과 같은 형식으로 입력해 주십시오: @person@instance.com"
|
moveAccountDescription: "새 계정으로 이전합니다.\n ・팔로워가 새 계정을 자동으로 팔로우 합니다\n ・이 계정에서 팔로우는 모두 해제됩니다\n ・이 계정으로는 노트 작성 등을 할 수 없게 됩니다\n\n팔로워는 자동으로 이전되지만, 팔로우는 수동으로 진행해야 합니다. 이전하기 전에 이 계정에서 팔로우를 내보내고, 이전 후에는 즉시 이전한 계정에서 가져오기를 진행하십시오.\n리스트・뮤트・차단에 대해서도 마찬가지이므로 수동으로 이전해야 합니다.\n\n(이 설명은 이 서버(Misskey v13.12.0 이후)의 사양입니다. Mastodon 등의 다른 ActivityPub 소프트웨어에서는 작동이 다를 수 있습니다.)"
|
||||||
moveAccountHowTo: "계정을 이사하려면 우선 이사갈 계정에서 이 계정에 대한 별칭을 지정해야 합니다.\n별칭을 작성한 다음, 이사갈 계정을 다음과 같이 입력하십시오:\n@username@server.example.com"
|
moveAccountHowTo: "계정을 이사하려면 우선 이사갈 계정에서 이 계정에 대한 별칭을 지정해야 합니다.\n별칭을 작성한 다음, 이사갈 계정을 다음과 같이 입력하십시오:\n@username@server.example.com"
|
||||||
startMigration: "이사하기"
|
startMigration: "이사하기"
|
||||||
migrationConfirm: "정말로 이 계정을 {account} 으로 이전하시겠습니까? 한 번 이전한 다음에는 취소할 수 없으며, 두 번 다시 원래 상태로 복구할 수 없습니다.\n이사할 계정에서 계정 별칭을 지정하였는지 다시 한 번 확인하십시오."
|
migrationConfirm: "정말로 이 계정을 {account} 으로 이전하시겠습니까? 한 번 이전한 다음에는 취소할 수 없으며, 두 번 다시 원래 상태로 복구할 수 없습니다.\n이사할 계정에서 계정 별칭을 지정하였는지 다시 한 번 확인하십시오."
|
||||||
@ -1491,7 +1516,7 @@ _menuDisplay:
|
|||||||
hide: "숨기기"
|
hide: "숨기기"
|
||||||
_wordMute:
|
_wordMute:
|
||||||
muteWords: "뮤트할 단어"
|
muteWords: "뮤트할 단어"
|
||||||
muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다。"
|
muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다."
|
||||||
muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요."
|
muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요."
|
||||||
softDescription: "지정한 조건의 노트를 타임라인에서 숨깁니다."
|
softDescription: "지정한 조건의 노트를 타임라인에서 숨깁니다."
|
||||||
hardDescription: "지정한 조건의 노트를 타임라인에 추가하지 않습니다. 타임라인에 추가되지 않은 노트는 조건을 변경해도 표시되지 않습니다."
|
hardDescription: "지정한 조건의 노트를 타임라인에 추가하지 않습니다. 타임라인에 추가되지 않은 노트는 조건을 변경해도 표시되지 않습니다."
|
||||||
|
@ -1,21 +1,31 @@
|
|||||||
---
|
---
|
||||||
_lang_: "Norsk Bokmål"
|
_lang_: "Norsk Bokmål"
|
||||||
|
headlineMisskey: "Et nettverk forbundet med Notes"
|
||||||
|
introMisskey: "Velkommen! Misskey er en desentralisert mikrobloggtjeneste med åpen kildekode.\nOpprett \"Notes\" for å dele tankene dine med alle rundt deg. 📡\nMed \"reaksjoner\" kan du også raskt gi uttrykk for hva du synes om alles Notes. 👍\nLa oss utforske en ny verden! 🚀"
|
||||||
|
monthAndDay: "{day}-{month}"
|
||||||
search: "Søk"
|
search: "Søk"
|
||||||
notifications: "Varsler"
|
notifications: "Varsler"
|
||||||
username: "Brukernavn"
|
username: "Brukernavn"
|
||||||
password: "Passord"
|
password: "Passord"
|
||||||
forgotPassword: "Glemt passord"
|
forgotPassword: "Glemt passord"
|
||||||
|
fetchingAsApObject: "Henter fra Fediverse..."
|
||||||
ok: "OK"
|
ok: "OK"
|
||||||
gotIt: "Skjønner"
|
gotIt: "Skjønner"
|
||||||
cancel: "Avbryt"
|
cancel: "Avbryt"
|
||||||
noThankYou: "Avbryt"
|
noThankYou: "Ikke nå"
|
||||||
|
enterUsername: "Skriv inn brukernavn"
|
||||||
|
renotedBy: "Renotes av {user}"
|
||||||
|
noNotes: "Ingen Notes"
|
||||||
noNotifications: "Ingen varsler"
|
noNotifications: "Ingen varsler"
|
||||||
instance: "Server"
|
instance: "Server"
|
||||||
settings: "Innstillinger"
|
settings: "Innstillinger"
|
||||||
notificationSettings: "Varslingsinnstillinger"
|
notificationSettings: "Varslingsinnstillinger"
|
||||||
|
basicSettings: "Grunnleggende innstillinger"
|
||||||
otherSettings: "Andre innstillinger"
|
otherSettings: "Andre innstillinger"
|
||||||
|
openInWindow: "Åpne i vindu"
|
||||||
profile: "Profil"
|
profile: "Profil"
|
||||||
timeline: "Tidslinje"
|
timeline: "Tidslinje"
|
||||||
|
noAccountDescription: "Denne brukeren har ikke skrevet sin biografi ennå."
|
||||||
login: "Logg inn"
|
login: "Logg inn"
|
||||||
loggingIn: "Logget inn"
|
loggingIn: "Logget inn"
|
||||||
logout: "Logg ut"
|
logout: "Logg ut"
|
||||||
@ -24,17 +34,21 @@ uploading: "Laster opp"
|
|||||||
save: "Lagre"
|
save: "Lagre"
|
||||||
users: "Brukere"
|
users: "Brukere"
|
||||||
addUser: "Legg til bruker"
|
addUser: "Legg til bruker"
|
||||||
favorite: "Favoritt"
|
favorite: "Legg til i favoritter"
|
||||||
favorites: "Favoritter"
|
favorites: "Favoritter"
|
||||||
unfavorite: "Fjern favoritt"
|
unfavorite: "Fjern fra favoritter"
|
||||||
pin: "Fest"
|
favorited: "Lagt til i favoritter."
|
||||||
unpin: "Opphev festing"
|
alreadyFavorited: "Allerede lagt til i favoritter."
|
||||||
|
cantFavorite: "Kunne ikke legge til i favoritter."
|
||||||
|
pin: "Fest til profil"
|
||||||
|
unpin: "Fjern fra profil"
|
||||||
copyContent: "Kopier innhold"
|
copyContent: "Kopier innhold"
|
||||||
copyLink: "Kopier lenke"
|
copyLink: "Kopier lenke"
|
||||||
delete: "Slett"
|
delete: "Slett"
|
||||||
deleteAndEdit: "Slett og rediger"
|
deleteAndEdit: "Slett og rediger"
|
||||||
|
deleteAndEditConfirm: "Er du sikker på at du vil slette denne Noten og redigere den? Du vil miste alle reaksjoner, Renotes og svar på den."
|
||||||
addToList: "Legg til i liste"
|
addToList: "Legg til i liste"
|
||||||
sendMessage: "Send melding"
|
sendMessage: "Send en melding"
|
||||||
copyRSS: "Kopier RSS"
|
copyRSS: "Kopier RSS"
|
||||||
copyUsername: "Kopier brukernavn"
|
copyUsername: "Kopier brukernavn"
|
||||||
searchUser: "Søk brukere"
|
searchUser: "Søk brukere"
|
||||||
@ -42,140 +56,290 @@ reply: "Svar"
|
|||||||
loadMore: "Vis mer"
|
loadMore: "Vis mer"
|
||||||
showMore: "Vis mer"
|
showMore: "Vis mer"
|
||||||
showLess: "Lukk"
|
showLess: "Lukk"
|
||||||
|
youGotNewFollower: "fulgte deg"
|
||||||
|
followRequestAccepted: "Følgeforespørsel akseptert"
|
||||||
|
importAndExport: "Importer og eksporter"
|
||||||
|
import: "Importer"
|
||||||
|
export: "Eksporter"
|
||||||
files: "Filer"
|
files: "Filer"
|
||||||
download: "Nedlastinger"
|
download: "Nedlastinger"
|
||||||
|
driveFileDeleteConfirm: "Er du sikker på at du vil slette \"{name}\"? Det vil også forsvinne fra alt innhold som bruker det."
|
||||||
|
unfollowConfirm: "Er du sikker på at du vil slutte å følge {name}?"
|
||||||
|
importRequested: "Du har bedt om import. Dette kan ta en stund."
|
||||||
lists: "Lister"
|
lists: "Lister"
|
||||||
noLists: "Ingen lister"
|
noLists: "Ingen lister"
|
||||||
following: "Følg"
|
note: "Note"
|
||||||
|
notes: "Notes"
|
||||||
|
following: "Følger"
|
||||||
followers: "Følgere"
|
followers: "Følgere"
|
||||||
followsYou: "Følger deg"
|
followsYou: "Følger deg"
|
||||||
createList: "Opprett liste"
|
createList: "Opprett liste"
|
||||||
error: "Feil"
|
error: "Feil"
|
||||||
|
somethingHappened: "En feil har oppstått"
|
||||||
retry: "Prøv igjen"
|
retry: "Prøv igjen"
|
||||||
pageLoadError: "Kunne ikke hente side."
|
pageLoadError: "Kunne ikke hente side."
|
||||||
|
serverIsDead: "Denne serveren svarer ikke. Vennligst vent en stund og prøv igjen."
|
||||||
|
enterListName: "Skriv inn et navn på listen"
|
||||||
privacy: "Personvern"
|
privacy: "Personvern"
|
||||||
|
defaultNoteVisibility: "Standard synlighet"
|
||||||
follow: "Følg"
|
follow: "Følg"
|
||||||
followRequest: "Følgeforespørsel"
|
followRequest: "Følgeforespørsel"
|
||||||
followRequests: "Følgeforespørsel"
|
followRequests: "Følgeforespørsel"
|
||||||
unfollow: "Avfølg"
|
unfollow: "Avfølg"
|
||||||
followRequestPending: "Venter på godkjenning"
|
followRequestPending: "Venter på godkjenning"
|
||||||
|
enterEmoji: "Skriv inn en emoji"
|
||||||
|
renote: "Renote"
|
||||||
|
renoted: "Renotet."
|
||||||
|
cantRenote: "Dette innlegget kan ikke renotes."
|
||||||
|
cantReRenote: "En Renote kan ikke renotes."
|
||||||
quote: "Sitat"
|
quote: "Sitat"
|
||||||
pinned: "Fest"
|
inChannelRenote: "Renote kun for kanal"
|
||||||
|
inChannelQuote: "Sitat kun for kanal"
|
||||||
|
pinnedNote: "Festet Note"
|
||||||
|
pinned: "Fest til profil"
|
||||||
you: "Du"
|
you: "Du"
|
||||||
clickToShow: "Klikk for å vise"
|
clickToShow: "Klikk for å vise"
|
||||||
add: "Legg til"
|
add: "Legg til"
|
||||||
reaction: "Reaksjon"
|
reaction: "Reaksjon"
|
||||||
reactions: "Reaksjoner"
|
reactions: "Reaksjoner"
|
||||||
|
reactionSetting: "Reaksjoner som vises i reaksjonsvelgeren"
|
||||||
|
reactionSettingDescription2: "Dra for å endre rekkefølgen, klikk for å slette, trykk \"+\" for å legge til."
|
||||||
|
rememberNoteVisibility: "Husk innstillingene for synlighet av Notes"
|
||||||
|
attachCancel: "Fjern vedlegg"
|
||||||
|
enterFileName: "Skriv inn filnavn"
|
||||||
mute: "Skjul"
|
mute: "Skjul"
|
||||||
unmute: "Vis"
|
unmute: "Vis"
|
||||||
|
renoteMute: "Skjul Renotes"
|
||||||
|
renoteUnmute: "Vis Renotes"
|
||||||
block: "Blokker"
|
block: "Blokker"
|
||||||
unblock: "Opphev blokkering"
|
unblock: "Opphev blokkering"
|
||||||
blockConfirm: "Blokker?"
|
suspend: "Suspender"
|
||||||
selectList: "Velg liste"
|
blockConfirm: "Er du sikker på at du vil blokke denne kontoen?"
|
||||||
selectChannel: "Velg kanal"
|
unblockConfirm: "Er du sikker på at du vil oppheve blokkeringen av denne kontoen?"
|
||||||
|
suspendConfirm: "Er du sikker på at du vil suspendere denne kontoen?"
|
||||||
|
selectList: "Velg en liste"
|
||||||
|
selectChannel: "Velg en kanal"
|
||||||
|
selectAntenna: "Velg en antenne"
|
||||||
|
selectWidget: "Velg en widget"
|
||||||
|
editWidgets: "Rediger widgeter"
|
||||||
|
editWidgetsExit: "Ferdig"
|
||||||
emoji: "Emoji"
|
emoji: "Emoji"
|
||||||
emojis: "Emojier"
|
emojis: "Emojier"
|
||||||
addEmoji: "Legg til emoji"
|
addEmoji: "Legg til emoji"
|
||||||
|
settingGuide: "Anbefalte innstillinger"
|
||||||
|
flagAsBot: "Merk denne kontoen som en bot"
|
||||||
|
flagAsBotDescription: "Aktiver dette alternativet hvis denne kontoen styres av et program. Hvis det er aktivert, vil det fungere som et flagg for andre utviklere for å forhindre endeløse interaksjonskjeder med andre roboter og justere Misskeys interne systemer til å behandle denne kontoen som en bot."
|
||||||
|
flagAsCat: "Merk denne kontoen som en katt"
|
||||||
|
flagAsCatDescription: "Aktiver dette alternativet for å merke denne kontoen som en katt."
|
||||||
|
flagShowTimelineReplies: "Vis svar i tidslinje"
|
||||||
addAccount: "Legg til konto"
|
addAccount: "Legg til konto"
|
||||||
selectUser: "Velg bruker"
|
reloadAccountsList: "Last inn kontoliste på nytt"
|
||||||
instances: "Server"
|
loginFailed: "Kunne ikke logge inn"
|
||||||
|
general: "Generelt"
|
||||||
|
searchWith: "Søk: {q}"
|
||||||
|
youHaveNoLists: "Du har ingen lister"
|
||||||
|
followConfirm: "Er du sikker på at du vil følge {name}?"
|
||||||
|
host: "Vert"
|
||||||
|
selectUser: "Velg en bruker"
|
||||||
|
recipient: "Mottaker"
|
||||||
|
annotation: "Kommentarer"
|
||||||
|
federation: "Føderasjon"
|
||||||
|
instances: "Servere"
|
||||||
registeredAt: "Registrerte seg"
|
registeredAt: "Registrerte seg"
|
||||||
|
latestRequestReceivedAt: "Siste forespørsel mottatt"
|
||||||
|
latestStatus: "Siste status"
|
||||||
|
charts: "Diagrammer"
|
||||||
perHour: "Per time"
|
perHour: "Per time"
|
||||||
perDay: "Per dag"
|
perDay: "Per dag"
|
||||||
|
stopActivityDelivery: "Slutt å sende aktiviteter"
|
||||||
blockThisInstance: "Blokker denne serveren"
|
blockThisInstance: "Blokker denne serveren"
|
||||||
|
operations: "Operasjoner"
|
||||||
|
software: "Programvare"
|
||||||
version: "Versjon"
|
version: "Versjon"
|
||||||
|
metadata: "Metadata"
|
||||||
|
withNFiles: "{n} fil(er)"
|
||||||
|
network: "Nettverk"
|
||||||
|
instanceInfo: "Serverinformasjon"
|
||||||
statistics: "Statistikk"
|
statistics: "Statistikk"
|
||||||
clearQueue: "Tøm kø"
|
clearQueue: "Tøm kø"
|
||||||
clearQueueConfirmTitle: "Vil du tømme kø?"
|
clearQueueConfirmTitle: "Er du sikker på at du vil tømme køen?"
|
||||||
blockedInstances: "Blokkerte severe"
|
blockedInstances: "Blokkerte severe"
|
||||||
|
blockedInstancesDescription: "Skriv opp vertsnavnene til serverne du vil blokkere, atskilt med linjeskift. Serverne i listen vil ikke lenger kunne kommunisere med denne serveren."
|
||||||
muteAndBlock: "Skjul og blokker"
|
muteAndBlock: "Skjul og blokker"
|
||||||
mutedUsers: "Skjulte brukere"
|
mutedUsers: "Skjulte brukere"
|
||||||
blockedUsers: "Blokkerte brukere"
|
blockedUsers: "Blokkerte brukere"
|
||||||
|
noUsers: "Det er ingen brukere"
|
||||||
editProfile: "Rediger profil"
|
editProfile: "Rediger profil"
|
||||||
|
noteDeleteConfirm: "Er du sikker på at du vil slette denne Noten?"
|
||||||
pinLimitExceeded: "Du kan ikke feste flere."
|
pinLimitExceeded: "Du kan ikke feste flere."
|
||||||
noCustomEmojis: "Ingen emoji"
|
intro: "Installasjonen av Misskey er ferdig! Vennligst opprett en administratorkonto."
|
||||||
|
done: "Ferdig"
|
||||||
|
default: "Standard"
|
||||||
|
defaultValueIs: "Standard: {value}"
|
||||||
|
noCustomEmojis: "Det er ingen emoji"
|
||||||
|
noJobs: "Det er ingen jobber"
|
||||||
blocked: "Blokkert"
|
blocked: "Blokkert"
|
||||||
|
suspended: "Suspendert"
|
||||||
all: "Alle"
|
all: "Alle"
|
||||||
|
notResponding: "Svarer ikke"
|
||||||
changePassword: "Endre passord"
|
changePassword: "Endre passord"
|
||||||
security: "Sikkerhet"
|
security: "Sikkerhet"
|
||||||
|
retypedNotMatch: "Inngangene stemmer ikke overens."
|
||||||
|
currentPassword: "Nåværende passord"
|
||||||
newPassword: "Nytt passord"
|
newPassword: "Nytt passord"
|
||||||
newPasswordRetype: "Nytt passord (gjenta)"
|
newPasswordRetype: "Nytt passord (gjenta)"
|
||||||
|
attachFile: "Legg ved filer"
|
||||||
more: "Mer!"
|
more: "Mer!"
|
||||||
|
noSuchUser: "Bruker ikke funnet"
|
||||||
|
announcements: "Kunngjøringer"
|
||||||
remove: "Slett"
|
remove: "Slett"
|
||||||
removed: "Slettet"
|
removed: "Vellykket slettet"
|
||||||
|
removeAreYouSure: "Er du sikker på at du vil fjerne \"{x}\"?"
|
||||||
|
deleteAreYouSure: "Er du sikker på at du vil slette \"{x}\"?"
|
||||||
saved: "Lagret"
|
saved: "Lagret"
|
||||||
upload: "Laste opp"
|
upload: "Laste opp"
|
||||||
|
keepOriginalUploading: "Behold originalbildet"
|
||||||
|
fromUrl: "Fra URL"
|
||||||
|
uploadFromUrl: "Last opp fra en URL"
|
||||||
|
uploadFromUrlDescription: "URL til filen du vil laste opp"
|
||||||
explore: "Utforsk"
|
explore: "Utforsk"
|
||||||
messageRead: "Lest"
|
messageRead: "Lest"
|
||||||
agree: "Jeg godtar"
|
nUsersRead: "lest av {n}"
|
||||||
|
agreeTo: "Jeg godtar {0}"
|
||||||
|
agree: "Godta"
|
||||||
|
agreeBelow: "Jeg godtar følgende"
|
||||||
|
basicNotesBeforeCreateAccount: "Viktige merknader"
|
||||||
|
termsOfService: "Vilkår for bruk"
|
||||||
home: "Hjem"
|
home: "Hjem"
|
||||||
|
activity: "Aktivitet"
|
||||||
images: "Bilder"
|
images: "Bilder"
|
||||||
image: "Bilder"
|
image: "Bilde"
|
||||||
birthday: "Bursdag"
|
birthday: "Bursdag"
|
||||||
yearsOld: "{age} år gammel"
|
yearsOld: "{age} år gammel"
|
||||||
|
theme: "Temaer"
|
||||||
light: "Lys"
|
light: "Lys"
|
||||||
dark: "Mørk"
|
dark: "Mørk"
|
||||||
|
lightThemes: "Lyse temaer"
|
||||||
|
darkThemes: "Mørke temaer"
|
||||||
|
syncDeviceDarkMode: "Synkroniser mørkmodus med enhetens innstillinger"
|
||||||
fileName: "Filnavn"
|
fileName: "Filnavn"
|
||||||
selectFile: "Velg fil"
|
selectFile: "Velg en fil"
|
||||||
selectFiles: "Velg fil"
|
selectFiles: "Velg filer"
|
||||||
selectFolder: "Velg mappe"
|
selectFolder: "Velg en mappe"
|
||||||
selectFolders: "Velg mappe"
|
selectFolders: "Velg mapper"
|
||||||
renameFile: "Endre filnavn"
|
renameFile: "Endre filnavn"
|
||||||
folderName: "Mappenavn"
|
folderName: "Mappenavn"
|
||||||
createFolder: "Opprett mappe"
|
createFolder: "Opprett en mappe"
|
||||||
renameFolder: "Endre mappenavn"
|
renameFolder: "Endre mappenavn"
|
||||||
deleteFolder: "Slett mappe"
|
deleteFolder: "Slett denne mappen"
|
||||||
addFile: "Legg til fil"
|
addFile: "Legg til en fil"
|
||||||
emptyFolder: "Denne mappen er tom"
|
emptyFolder: "Denne mappen er tom"
|
||||||
|
unableToDelete: "Kan ikke slette"
|
||||||
|
inputNewFileName: "Skriv inn et nytt filnavn"
|
||||||
|
inputNewDescription: "Skriv inn ny bildetekst"
|
||||||
|
inputNewFolderName: "Skriv inn et nytt mappenavn"
|
||||||
|
circularReferenceFolder: "Målmappen er en undermappe til mappen du ønsker å flytte."
|
||||||
|
hasChildFilesOrFolders: "Siden denne mappen ikke er tom, kan den ikke slettes."
|
||||||
copyUrl: "Kopier URL"
|
copyUrl: "Kopier URL"
|
||||||
rename: "Endre navn"
|
rename: "Endre navn"
|
||||||
doNothing: "Gjør ingenting"
|
avatar: "Avatar"
|
||||||
|
banner: "Banner"
|
||||||
|
doNothing: "Ignorer"
|
||||||
accept: "Tillatt"
|
accept: "Tillatt"
|
||||||
reject: "Avslå"
|
reject: "Avslå"
|
||||||
instanceName: "Servernavn"
|
instanceName: "Servernavn"
|
||||||
thisYear: "I år"
|
instanceDescription: "Serverbeskrivelse"
|
||||||
|
thisYear: "År"
|
||||||
|
thisMonth: "Måned"
|
||||||
today: "I dag"
|
today: "I dag"
|
||||||
|
dayX: "{day}"
|
||||||
|
monthX: "{month}"
|
||||||
|
yearX: "{year}"
|
||||||
pages: "Sider"
|
pages: "Sider"
|
||||||
pinnedUsers: "Festete brukrere"
|
integration: "Integrasjon"
|
||||||
pinnedPages: "Festete sider"
|
enableLocalTimeline: "Aktiver lokal tidslinje"
|
||||||
|
enableGlobalTimeline: "Aktiver global tidslinje"
|
||||||
|
disablingTimelinesInfo: "Administratorer og Moderatorer vil alltid ha tilgang til alle tidslinjer, selv om de ikke er aktivert."
|
||||||
|
registration: "Registrer"
|
||||||
|
enableRegistration: "Aktiver registrering av nye brukere"
|
||||||
|
invite: "Inviter"
|
||||||
|
basicInfo: "Grunnleggende informasjon"
|
||||||
|
pinnedUsers: "Festede brukrere"
|
||||||
|
pinnedUsersDescription: "Liste over brukernavn atskilt med linjeskift som skal festes i \"Utforsk\" fanen."
|
||||||
|
pinnedPages: "Festede sider"
|
||||||
|
pinnedNotes: "Festet Note"
|
||||||
hcaptcha: "hCaptcha"
|
hcaptcha: "hCaptcha"
|
||||||
|
enableHcaptcha: "Aktiver hCaptcha"
|
||||||
recaptcha: "reCAPTCHA"
|
recaptcha: "reCAPTCHA"
|
||||||
|
enableRecaptcha: "Aktiver reCAPTCHA"
|
||||||
|
turnstile: "Turnstile"
|
||||||
|
enableTurnstile: "Aktiver Turnstile"
|
||||||
|
antennas: "Antenner"
|
||||||
name: "Navn"
|
name: "Navn"
|
||||||
|
antennaSource: "Antennekilde"
|
||||||
|
notifyAntenna: "Varsle om nye Notes"
|
||||||
|
withFileAntenna: "Bare Notes med filer"
|
||||||
|
notesAndReplies: "Notes og svar"
|
||||||
popularUsers: "Populære brukere"
|
popularUsers: "Populære brukere"
|
||||||
exploreUsersCount: "Det finnes {count} brukere"
|
exploreUsersCount: "Det finnes {count} brukere"
|
||||||
|
exploreFediverse: "Utforsk Fediverse"
|
||||||
userList: "Lister"
|
userList: "Lister"
|
||||||
about: "Infomasjon"
|
about: "Informasjon"
|
||||||
aboutMisskey: "Om Misskey"
|
aboutMisskey: "Om Misskey"
|
||||||
|
newPasswordIs: "Det nye passordet er \"{password}\"."
|
||||||
share: "Del"
|
share: "Del"
|
||||||
|
notFound: "Ikke funnet"
|
||||||
|
markAsReadAllNotifications: "Merk alle varsler som lest"
|
||||||
|
markAsReadAllUnreadNotes: "Merk alle Notes som lest"
|
||||||
help: "Hjelp"
|
help: "Hjelp"
|
||||||
|
inputMessageHere: "Skriv inn melding her"
|
||||||
close: "Lukk"
|
close: "Lukk"
|
||||||
|
invites: "Inviter"
|
||||||
members: "Medlemmer"
|
members: "Medlemmer"
|
||||||
|
title: "Tittel"
|
||||||
text: "Tekst"
|
text: "Tekst"
|
||||||
next: "Neste"
|
next: "Neste"
|
||||||
retype: "Gjenta"
|
retype: "Gjenta"
|
||||||
|
quoteAttached: "Sitat"
|
||||||
|
noMessagesYet: "Ingen meldinger ennå"
|
||||||
|
newMessageExists: "Det er nye meldinger"
|
||||||
|
onlyOneFileCanBeAttached: "Du kan bare legge ved én fil i en melding"
|
||||||
|
invitations: "Inviter"
|
||||||
available: "Tilgjengelig"
|
available: "Tilgjengelig"
|
||||||
unavailable: "Utilgjengelig"
|
unavailable: "Utilgjengelig"
|
||||||
tooShort: "For kort"
|
tooShort: "For kort"
|
||||||
tooLong: "For langt"
|
tooLong: "For langt"
|
||||||
|
weakPassword: "Svakt passord"
|
||||||
|
normalPassword: "Gjennomsnittlig passord"
|
||||||
|
strongPassword: "Sterkt passord"
|
||||||
|
signinWith: "Logg inn med {x}"
|
||||||
|
signinFailed: "Kunne ikke logge inn. Det oppgitte brukernavnet eller passordet er feil."
|
||||||
or: "eller"
|
or: "eller"
|
||||||
language: "Språk"
|
language: "Språk"
|
||||||
aboutX: "Om {x}"
|
aboutX: "Om {x}"
|
||||||
category: "Kategorier"
|
category: "Kategori"
|
||||||
createAccount: "Opprett konto"
|
createAccount: "Opprett konto"
|
||||||
|
openImageInNewTab: "Åpne bilder i ny fane"
|
||||||
|
clientSettings: "Klientinnstillinger"
|
||||||
|
accountSettings: "Kontoinnstillinger"
|
||||||
objectStorageRegion: "Region"
|
objectStorageRegion: "Region"
|
||||||
objectStorageUseSSL: "Bruk SSL"
|
objectStorageUseSSL: "Bruk SSL"
|
||||||
objectStorageUseProxy: "Bruk Proxy"
|
objectStorageUseProxy: "Bruk Proxy"
|
||||||
deleteAll: "Slett alt"
|
deleteAll: "Slett alt"
|
||||||
|
newNoteRecived: "Det er nye Notes"
|
||||||
listen: "Lytt"
|
listen: "Lytt"
|
||||||
none: "Ingen"
|
none: "Ingen"
|
||||||
|
volume: "Volum"
|
||||||
chooseEmoji: "Velg emoji"
|
chooseEmoji: "Velg emoji"
|
||||||
recentUsed: "Sist brukte"
|
recentUsed: "Sist brukte"
|
||||||
install: "Installer"
|
install: "Installer"
|
||||||
|
uninstall: "Avinstaller"
|
||||||
nothing: "Ingenting"
|
nothing: "Ingenting"
|
||||||
deleteAllFiles: "Slett alle filer"
|
deleteAllFiles: "Slett alle filer"
|
||||||
deleteAllFilesConfirm: "Vil du slette alle filer?"
|
deleteAllFilesConfirm: "Er du sikker på at du vil slette alle filer?"
|
||||||
|
userSuspended: "Denne brukeren har blitt suspendert."
|
||||||
accountDeleted: "Kontoen blir slettet"
|
accountDeleted: "Kontoen blir slettet"
|
||||||
accountDeletedDescription: "Denne kontoen blir slettet"
|
accountDeletedDescription: "Denne kontoen har blitt slettet."
|
||||||
menu: "Meny"
|
menu: "Meny"
|
||||||
poll: "Avstemning"
|
poll: "Avstemning"
|
||||||
description: "Beskrivelse"
|
description: "Beskrivelse"
|
||||||
@ -186,6 +350,7 @@ small: "Liten"
|
|||||||
notificationType: "Varseltype"
|
notificationType: "Varseltype"
|
||||||
edit: "Rediger"
|
edit: "Rediger"
|
||||||
email: "E-post"
|
email: "E-post"
|
||||||
|
smtpHost: "Vert"
|
||||||
smtpUser: "Brukernavn"
|
smtpUser: "Brukernavn"
|
||||||
smtpPass: "Passord"
|
smtpPass: "Passord"
|
||||||
userSaysSomething: "{name} sa noe"
|
userSaysSomething: "{name} sa noe"
|
||||||
@ -201,16 +366,25 @@ reportAbuse: "Rappoter"
|
|||||||
send: "Send"
|
send: "Send"
|
||||||
openInNewTab: "Åpne i ny fane"
|
openInNewTab: "Åpne i ny fane"
|
||||||
waitingFor: "Venter på {x}"
|
waitingFor: "Venter på {x}"
|
||||||
|
random: "Tilfeldig"
|
||||||
system: "System"
|
system: "System"
|
||||||
|
desktop: "Skrivebord"
|
||||||
|
i18nInfo: "Misskey oversettes til flere språk av frivillige. Du kan hjelpe til på {link}."
|
||||||
followingCount: "Følger"
|
followingCount: "Følger"
|
||||||
followersCount: "Følgere"
|
followersCount: "Følgere"
|
||||||
yes: "Ja"
|
yes: "Ja"
|
||||||
no: "Nei"
|
no: "Nei"
|
||||||
|
contact: "Kontakt"
|
||||||
|
developer: "Utvikler"
|
||||||
|
makeExplorable: "Gjør konto synlig i \"Utforsk\""
|
||||||
|
makeExplorableDescription: "Hvis du slår av dette, vises ikke kontoen din i \"Utforsk\" delen."
|
||||||
left: "Venstre"
|
left: "Venstre"
|
||||||
|
nNotes: "{n} Notes"
|
||||||
saveAs: "Lagre som"
|
saveAs: "Lagre som"
|
||||||
value: "Verdi"
|
value: "Verdi"
|
||||||
deleteConfirm: "Vil du slette?"
|
deleteConfirm: "Vil du slette?"
|
||||||
invalidValue: "Verdien er ugyldig."
|
invalidValue: "Verdien er ugyldig."
|
||||||
|
closeAccount: "Avslutt konto"
|
||||||
emailNotification: "E-postvarsler"
|
emailNotification: "E-postvarsler"
|
||||||
inChannelSearch: "Søk i kanal"
|
inChannelSearch: "Søk i kanal"
|
||||||
clear: "Tøm"
|
clear: "Tøm"
|
||||||
@ -224,16 +398,23 @@ accounts: "Kontoer"
|
|||||||
switch: "Bytt"
|
switch: "Bytt"
|
||||||
gallery: "Galleri"
|
gallery: "Galleri"
|
||||||
ads: "Annonser"
|
ads: "Annonser"
|
||||||
|
memo: "Notat"
|
||||||
high: "Høy"
|
high: "Høy"
|
||||||
low: "Lav"
|
low: "Lav"
|
||||||
sent: "Send"
|
sent: "Sendt"
|
||||||
|
received: "Mottatt"
|
||||||
learnMore: "Les mer"
|
learnMore: "Les mer"
|
||||||
|
misskeyUpdated: "Misskey har blitt oppdatert!"
|
||||||
translate: "Oversett"
|
translate: "Oversett"
|
||||||
|
translatedFrom: "Oversatt fra {x}"
|
||||||
unread: "Ulest"
|
unread: "Ulest"
|
||||||
manageAccounts: "Administrer konto"
|
manageAccounts: "Administrer konto"
|
||||||
classic: "Klassisk"
|
classic: "Klassisk"
|
||||||
muteThread: "Skjul denne tråden"
|
muteThread: "Skjul denne tråden"
|
||||||
unmuteThread: "Vis denne tråden"
|
unmuteThread: "Vis denne tråden"
|
||||||
|
continueThread: "Vis fortsettelse av tråden"
|
||||||
|
hide: "Skjul"
|
||||||
|
smartphone: "Smarttelefon"
|
||||||
tablet: "Nettbrett"
|
tablet: "Nettbrett"
|
||||||
auto: "Automatisk"
|
auto: "Automatisk"
|
||||||
size: "Størrelse"
|
size: "Størrelse"
|
||||||
@ -249,10 +430,10 @@ check: "Sjekk"
|
|||||||
deleteAccount: "Slett konto"
|
deleteAccount: "Slett konto"
|
||||||
document: "Dokumenter"
|
document: "Dokumenter"
|
||||||
logoutConfirm: "Vil du logge ut?"
|
logoutConfirm: "Vil du logge ut?"
|
||||||
pleaseSelect: "Vennligst velg"
|
pleaseSelect: "Velg et alternativ"
|
||||||
type: "Type"
|
type: "Type"
|
||||||
beta: "Beta"
|
beta: "Beta"
|
||||||
account: "Kontoer"
|
account: "Konto"
|
||||||
move: "Flytt"
|
move: "Flytt"
|
||||||
pushNotification: "Push-varsler"
|
pushNotification: "Push-varsler"
|
||||||
tools: "Verktøy"
|
tools: "Verktøy"
|
||||||
@ -268,6 +449,7 @@ role: "Rolle"
|
|||||||
color: "Farge"
|
color: "Farge"
|
||||||
youCannotCreateAnymore: "Du kan ikke opprette flere."
|
youCannotCreateAnymore: "Du kan ikke opprette flere."
|
||||||
cannotPerformTemporary: "Midlertidig utilgjengelig"
|
cannotPerformTemporary: "Midlertidig utilgjengelig"
|
||||||
|
achievements: "Prestasjoner"
|
||||||
thisPostMayBeAnnoyingCancel: "Avbryt"
|
thisPostMayBeAnnoyingCancel: "Avbryt"
|
||||||
exploreOtherServers: "Utforsk andre severe"
|
exploreOtherServers: "Utforsk andre severe"
|
||||||
letsLookAtTimeline: "La oss se på tidslinje"
|
letsLookAtTimeline: "La oss se på tidslinje"
|
||||||
@ -283,6 +465,26 @@ _initialAccountSetting:
|
|||||||
theseSettingsCanEditLater: "Du kan endre disse innstillingene senere."
|
theseSettingsCanEditLater: "Du kan endre disse innstillingene senere."
|
||||||
_achievements:
|
_achievements:
|
||||||
_types:
|
_types:
|
||||||
|
_notes10:
|
||||||
|
title: "Noen Notes"
|
||||||
|
_notes100:
|
||||||
|
title: "Mange Notes"
|
||||||
|
_notes500:
|
||||||
|
title: "Dekket i Notes"
|
||||||
|
_notes1000:
|
||||||
|
title: "Et fjell av Notes"
|
||||||
|
_notes5000:
|
||||||
|
title: "Overfylte Notes"
|
||||||
|
_notes10000:
|
||||||
|
title: "Super Notes"
|
||||||
|
_notes20000:
|
||||||
|
title: "Trenger... mer... Notes..."
|
||||||
|
_notes30000:
|
||||||
|
title: "Notes Notes Notes!"
|
||||||
|
_notes40000:
|
||||||
|
title: "Note fabrikk"
|
||||||
|
_notes50000:
|
||||||
|
title: "Planet av Notes"
|
||||||
_notes100000:
|
_notes100000:
|
||||||
flavor: "Du har jammen mye å si."
|
flavor: "Du har jammen mye å si."
|
||||||
_noteFavorited1:
|
_noteFavorited1:
|
||||||
@ -311,11 +513,25 @@ _achievements:
|
|||||||
_justPlainLucky:
|
_justPlainLucky:
|
||||||
title: "Rett og slett heldig"
|
title: "Rett og slett heldig"
|
||||||
_setNameToSyuilo:
|
_setNameToSyuilo:
|
||||||
description: "Du har satt navnet ditt til \"syuilo\""
|
description: "Du satte navnet ditt til \"syuilo\""
|
||||||
|
_passedSinceAccountCreated1:
|
||||||
|
title: "Ett års jubileum"
|
||||||
|
description: "Det har gått ett år siden kontoen din ble opprettet"
|
||||||
|
_passedSinceAccountCreated2:
|
||||||
|
title: "To års jubileum"
|
||||||
|
description: "Det har gått to år siden kontoen din ble opprettet"
|
||||||
|
_passedSinceAccountCreated3:
|
||||||
|
title: "Tre års jubileum"
|
||||||
|
description: "Det har gått tre år siden kontoen din ble opprettet"
|
||||||
_loggedInOnBirthday:
|
_loggedInOnBirthday:
|
||||||
title: "Gratulerer med dagen"
|
title: "Gratulerer med dagen"
|
||||||
|
description: "Du logget inn på bursdagen din"
|
||||||
_loggedInOnNewYearsDay:
|
_loggedInOnNewYearsDay:
|
||||||
title: "Godt nytt år"
|
title: "Godt nytt år"
|
||||||
|
description: "Du logget inn på årets første dag"
|
||||||
|
_cookieClicked:
|
||||||
|
description: "Du klikket på kjeksen"
|
||||||
|
flavor: "Er du på riktig nettsted?"
|
||||||
_brainDiver:
|
_brainDiver:
|
||||||
title: "Brain Diver"
|
title: "Brain Diver"
|
||||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||||
@ -333,6 +549,9 @@ _ad:
|
|||||||
_gallery:
|
_gallery:
|
||||||
like: "Liker!"
|
like: "Liker!"
|
||||||
unlike: "Liker ikke"
|
unlike: "Liker ikke"
|
||||||
|
_email:
|
||||||
|
_follow:
|
||||||
|
title: "fulgte deg"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
saveNew: "Lagre som ny"
|
saveNew: "Lagre som ny"
|
||||||
cannotSave: "Kunne ikke lagre"
|
cannotSave: "Kunne ikke lagre"
|
||||||
@ -351,6 +570,8 @@ _channel:
|
|||||||
featured: "Populært"
|
featured: "Populært"
|
||||||
following: "Følger"
|
following: "Følger"
|
||||||
nameAndDescription: "Navn og beskrivelse"
|
nameAndDescription: "Navn og beskrivelse"
|
||||||
|
_menuDisplay:
|
||||||
|
hide: "Skjul"
|
||||||
_wordMute:
|
_wordMute:
|
||||||
soft: "Myk"
|
soft: "Myk"
|
||||||
hard: "Hard"
|
hard: "Hard"
|
||||||
@ -360,15 +581,17 @@ _theme:
|
|||||||
key: "Nøkkel"
|
key: "Nøkkel"
|
||||||
keys:
|
keys:
|
||||||
link: "Lenke"
|
link: "Lenke"
|
||||||
|
renote: "Renote"
|
||||||
_sfx:
|
_sfx:
|
||||||
|
note: "Notes"
|
||||||
notification: "Varsler"
|
notification: "Varsler"
|
||||||
_ago:
|
_ago:
|
||||||
future: "Fremitid"
|
future: "Fremitid"
|
||||||
justNow: "Akkurat nå"
|
justNow: "Akkurat nå"
|
||||||
secondsAgo: "{n} sekunder siden"
|
secondsAgo: "{n}s siden"
|
||||||
minutesAgo: "{n} minutter siden"
|
minutesAgo: "{n}m siden"
|
||||||
hoursAgo: "{n} timer siden"
|
hoursAgo: "{n}t siden"
|
||||||
daysAgo: "{n} dager siden"
|
daysAgo: "{n}d siden"
|
||||||
weeksAgo: "{n} uker siden"
|
weeksAgo: "{n} uker siden"
|
||||||
monthsAgo: "{n} måneder siden"
|
monthsAgo: "{n} måneder siden"
|
||||||
yearsAgo: "{n} år siden"
|
yearsAgo: "{n} år siden"
|
||||||
@ -380,6 +603,7 @@ _time:
|
|||||||
day: "Dager"
|
day: "Dager"
|
||||||
_timelineTutorial:
|
_timelineTutorial:
|
||||||
title: "Hvordan bruke Misskey"
|
title: "Hvordan bruke Misskey"
|
||||||
|
step2_2: "Hva med å skrive en selvpresentasjon, eller bare \"Hei {name}!\" hvis du ikke har lyst?"
|
||||||
_2fa:
|
_2fa:
|
||||||
renewTOTPCancel: "Avbryt"
|
renewTOTPCancel: "Avbryt"
|
||||||
_weekday:
|
_weekday:
|
||||||
@ -392,18 +616,22 @@ _weekday:
|
|||||||
saturday: "Lørdag"
|
saturday: "Lørdag"
|
||||||
_widgets:
|
_widgets:
|
||||||
profile: "Profil"
|
profile: "Profil"
|
||||||
|
instanceInfo: "Serverinformasjon"
|
||||||
notifications: "Varsler"
|
notifications: "Varsler"
|
||||||
timeline: "Tidslinje"
|
timeline: "Tidslinje"
|
||||||
calendar: "Kalender"
|
calendar: "Kalender"
|
||||||
trends: "Populært"
|
trends: "Populært"
|
||||||
clock: "Klokke"
|
clock: "Klokke"
|
||||||
|
activity: "Aktivitet"
|
||||||
photos: "Bilder"
|
photos: "Bilder"
|
||||||
|
federation: "Føderasjon"
|
||||||
button: "Knapp"
|
button: "Knapp"
|
||||||
aiscriptApp: "AiScript App"
|
aiscriptApp: "AiScript App"
|
||||||
userList: "Brukerliste"
|
userList: "Brukerliste"
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "Velg liste"
|
chooseList: "Velg liste"
|
||||||
_cw:
|
_cw:
|
||||||
|
hide: "Skjul"
|
||||||
show: "Vis mer"
|
show: "Vis mer"
|
||||||
_poll:
|
_poll:
|
||||||
noOnlyOneChoice: "Trenger minst to valger."
|
noOnlyOneChoice: "Trenger minst to valger."
|
||||||
@ -424,6 +652,7 @@ _postForm:
|
|||||||
_profile:
|
_profile:
|
||||||
name: "Navn"
|
name: "Navn"
|
||||||
username: "Brukernavn"
|
username: "Brukernavn"
|
||||||
|
description: "Biografi"
|
||||||
metadataContent: "Innhold"
|
metadataContent: "Innhold"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
followingList: "Følg"
|
followingList: "Følg"
|
||||||
@ -431,6 +660,7 @@ _exportOrImport:
|
|||||||
blockingList: "Blokker"
|
blockingList: "Blokker"
|
||||||
userLists: "Lister"
|
userLists: "Lister"
|
||||||
_charts:
|
_charts:
|
||||||
|
federation: "Føderasjon"
|
||||||
filesIncDec: "Forskjell på antall filer"
|
filesIncDec: "Forskjell på antall filer"
|
||||||
_instanceCharts:
|
_instanceCharts:
|
||||||
users: "Forskjell på antall brukere"
|
users: "Forskjell på antall brukere"
|
||||||
@ -442,14 +672,18 @@ _play:
|
|||||||
new: "Opprett Play"
|
new: "Opprett Play"
|
||||||
edit: "Rediger Play"
|
edit: "Rediger Play"
|
||||||
featured: "Populært"
|
featured: "Populært"
|
||||||
|
title: "Tittel"
|
||||||
summary: "Beskrivelse"
|
summary: "Beskrivelse"
|
||||||
_pages:
|
_pages:
|
||||||
|
invalidNameText: "Pass på at sidetittelen ikke er tom"
|
||||||
like: "Liker"
|
like: "Liker"
|
||||||
unlike: "Liker ikke"
|
unlike: "Liker ikke"
|
||||||
my: "Mine sider"
|
my: "Mine sider"
|
||||||
featured: "Populært"
|
featured: "Populært"
|
||||||
contents: "Innhold"
|
contents: "Innhold"
|
||||||
|
title: "Tittel"
|
||||||
url: "Side URL"
|
url: "Side URL"
|
||||||
|
hideTitleWhenPinned: "Skjul sidetittel når festet til profil"
|
||||||
fontSerif: "Serif"
|
fontSerif: "Serif"
|
||||||
fontSansSerif: "Sans Serif"
|
fontSansSerif: "Sans Serif"
|
||||||
selectType: "Velg type"
|
selectType: "Velg type"
|
||||||
@ -459,13 +693,18 @@ _pages:
|
|||||||
image: "Bilde"
|
image: "Bilde"
|
||||||
button: "Knapp"
|
button: "Knapp"
|
||||||
_notification:
|
_notification:
|
||||||
|
youWereFollowed: "fulgte deg"
|
||||||
|
unreadAntennaNote: "Antenne {name}"
|
||||||
|
achievementEarned: "Prestasjon låst opp"
|
||||||
_types:
|
_types:
|
||||||
follow: "Følg"
|
follow: "Nye følgere"
|
||||||
reply: "Svar"
|
reply: "Svar"
|
||||||
quote: "Sitat"
|
renote: "Renotes"
|
||||||
reaction: "Reaksjon"
|
quote: "Sitater"
|
||||||
|
reaction: "Reaksjoner"
|
||||||
_actions:
|
_actions:
|
||||||
reply: "Svar"
|
reply: "Svar"
|
||||||
|
renote: "Renote"
|
||||||
_deck:
|
_deck:
|
||||||
swapLeft: "Flytt til venstre"
|
swapLeft: "Flytt til venstre"
|
||||||
swapRight: "Flytt til høyre"
|
swapRight: "Flytt til høyre"
|
||||||
@ -477,6 +716,7 @@ _deck:
|
|||||||
_columns:
|
_columns:
|
||||||
notifications: "Varsler"
|
notifications: "Varsler"
|
||||||
tl: "Tidslinje"
|
tl: "Tidslinje"
|
||||||
|
antenna: "Antenner"
|
||||||
list: "Lister"
|
list: "Lister"
|
||||||
channel: "Kanaler"
|
channel: "Kanaler"
|
||||||
direct: "Direkte"
|
direct: "Direkte"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
_lang_: "Русский"
|
_lang_: "Русский"
|
||||||
headlineMisskey: "Сеть, сплетённая из заметок"
|
headlineMisskey: "Сеть, сплетённая из заметок"
|
||||||
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
|
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
|
||||||
poweredByMisskeyDescription: "{name} – один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом <b>Misskey</b>."
|
poweredByMisskeyDescription: "{name} – сервис на платформе с открытым исходным кодом <b>Misskey</b>, называемый инстансом Misskey."
|
||||||
monthAndDay: "{day}.{month}"
|
monthAndDay: "{day}.{month}"
|
||||||
search: "Поиск"
|
search: "Поиск"
|
||||||
notifications: "Уведомления"
|
notifications: "Уведомления"
|
||||||
@ -560,6 +560,7 @@ accountDeletedDescription: "Эта учетная запись удалена"
|
|||||||
menu: "Меню"
|
menu: "Меню"
|
||||||
divider: "Линия-разделитель"
|
divider: "Линия-разделитель"
|
||||||
addItem: "Добавить элемент"
|
addItem: "Добавить элемент"
|
||||||
|
rearrange: "Сортировать по"
|
||||||
relays: "Ретрансляторы"
|
relays: "Ретрансляторы"
|
||||||
addRelay: "Добавить ретранслятор"
|
addRelay: "Добавить ретранслятор"
|
||||||
inboxUrl: "URL ящика входящих сообщений"
|
inboxUrl: "URL ящика входящих сообщений"
|
||||||
@ -648,8 +649,8 @@ abuseReported: "Жалоба отправлена. Большое спасибо
|
|||||||
reporter: "Сообщивший"
|
reporter: "Сообщивший"
|
||||||
reporteeOrigin: "О ком сообщено"
|
reporteeOrigin: "О ком сообщено"
|
||||||
reporterOrigin: "Кто сообщил"
|
reporterOrigin: "Кто сообщил"
|
||||||
forwardReport: "Перенаправление отчета на инстант."
|
forwardReport: "Отправить жалобу на инстанс автора."
|
||||||
forwardReportIsAnonymous: "Удаленный инстант не сможет увидеть вашу информацию и будет отображаться как анонимная системная учетная запись."
|
forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись."
|
||||||
send: "Отправить"
|
send: "Отправить"
|
||||||
abuseMarkAsResolved: "Отметить жалобу как решённую"
|
abuseMarkAsResolved: "Отметить жалобу как решённую"
|
||||||
openInNewTab: "Открыть в новой вкладке"
|
openInNewTab: "Открыть в новой вкладке"
|
||||||
@ -822,6 +823,7 @@ translatedFrom: "Перевод. Язык оригинала — {x}"
|
|||||||
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
|
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
|
||||||
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
|
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
|
||||||
aiChanMode: "Режим Ай"
|
aiChanMode: "Режим Ай"
|
||||||
|
devMode: "Режим разработчика"
|
||||||
keepCw: "Сохраняйте Предупреждения о содержимом"
|
keepCw: "Сохраняйте Предупреждения о содержимом"
|
||||||
pubSub: "Учётные записи Pub/Sub"
|
pubSub: "Учётные записи Pub/Sub"
|
||||||
lastCommunication: "Последнее сообщение"
|
lastCommunication: "Последнее сообщение"
|
||||||
@ -913,8 +915,8 @@ cannotUploadBecauseInappropriate: "Файл не может быть загру
|
|||||||
cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске"
|
cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске"
|
||||||
cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла."
|
cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла."
|
||||||
beta: "Бета"
|
beta: "Бета"
|
||||||
enableAutoSensitive: "Автоматическое определение NSFW"
|
enableAutoSensitive: "Автоматическое определение содержимого не для всех"
|
||||||
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена автоматически в зависимости от инстанта."
|
enableAutoSensitiveDescription: "Позволяет определять наличие содержимого не для всех при помощи искусственного интеллекта там, где это возможно. Даже если эту опцию отключить, она всё равно может быть включена на весь инстанс."
|
||||||
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
|
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
|
||||||
navbar: "Панель навигации"
|
navbar: "Панель навигации"
|
||||||
shuffle: "Перемешать"
|
shuffle: "Перемешать"
|
||||||
@ -989,6 +991,7 @@ rolesAssignedToMe: "Мои роли"
|
|||||||
resetPasswordConfirm: "Сбросить пароль?"
|
resetPasswordConfirm: "Сбросить пароль?"
|
||||||
sensitiveWords: "Чувствительные слова"
|
sensitiveWords: "Чувствительные слова"
|
||||||
sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк."
|
sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк."
|
||||||
|
sensitiveWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение."
|
||||||
notesSearchNotAvailable: "Поиск заметок недоступен"
|
notesSearchNotAvailable: "Поиск заметок недоступен"
|
||||||
license: "Лицензия"
|
license: "Лицензия"
|
||||||
unfavoriteConfirm: "Удалить избранное?"
|
unfavoriteConfirm: "Удалить избранное?"
|
||||||
@ -1004,6 +1007,7 @@ noteIdOrUrl: "ID или ссылка на заметку"
|
|||||||
video: "Видео"
|
video: "Видео"
|
||||||
videos: "Видео"
|
videos: "Видео"
|
||||||
dataSaver: "Экономия трафика"
|
dataSaver: "Экономия трафика"
|
||||||
|
renotesList: "Репосты"
|
||||||
horizontal: "Сбоку"
|
horizontal: "Сбоку"
|
||||||
youFollowing: "Подписки"
|
youFollowing: "Подписки"
|
||||||
options: "Настройки ролей"
|
options: "Настройки ролей"
|
||||||
@ -1178,6 +1182,9 @@ _achievements:
|
|||||||
_client30min:
|
_client30min:
|
||||||
title: "Перерыв на обед"
|
title: "Перерыв на обед"
|
||||||
description: "Прошло 30 минут с момента запуска клиента"
|
description: "Прошло 30 минут с момента запуска клиента"
|
||||||
|
_client60min:
|
||||||
|
title: "Не наглядеться на Misskey"
|
||||||
|
description: "Misskey был открыт 60 минут подряд"
|
||||||
_noteDeletedWithin1min:
|
_noteDeletedWithin1min:
|
||||||
title: "Ой, нет!"
|
title: "Ой, нет!"
|
||||||
description: "Заметка удалена через минуту после публикации"
|
description: "Заметка удалена через минуту после публикации"
|
||||||
@ -1280,6 +1287,7 @@ _role:
|
|||||||
canInvite: "Может создавать пригласительные коды"
|
canInvite: "Может создавать пригласительные коды"
|
||||||
canManageCustomEmojis: "Управлять пользовательскими эмодзи"
|
canManageCustomEmojis: "Управлять пользовательскими эмодзи"
|
||||||
driveCapacity: "Доступное пространство на «диске»"
|
driveCapacity: "Доступное пространство на «диске»"
|
||||||
|
alwaysMarkNsfw: "Всегда отмечать файлы как «не для всех»"
|
||||||
pinMax: "Доступное количество закреплённых заметок"
|
pinMax: "Доступное количество закреплённых заметок"
|
||||||
antennaMax: "Доступное количество антенн"
|
antennaMax: "Доступное количество антенн"
|
||||||
wordMuteMax: "Доступное количество знаков в списке скрытия слов"
|
wordMuteMax: "Доступное количество знаков в списке скрытия слов"
|
||||||
@ -1307,7 +1315,7 @@ _sensitiveMediaDetection:
|
|||||||
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
|
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
|
||||||
sensitivity: "Чувствительность обнаружения"
|
sensitivity: "Чувствительность обнаружения"
|
||||||
sensitivityDescription: "Более низкая чувствительность уменьшает количество ложных срабатываний (false positives). Повышение чувствительности уменьшает утечку при обнаружении (ложноотрицательные результаты)."
|
sensitivityDescription: "Более низкая чувствительность уменьшает количество ложных срабатываний (false positives). Повышение чувствительности уменьшает утечку при обнаружении (ложноотрицательные результаты)."
|
||||||
setSensitiveFlagAutomatically: "Установить флаг NSFW"
|
setSensitiveFlagAutomatically: "Обозначить как не для всех"
|
||||||
setSensitiveFlagAutomaticallyDescription: "Даже если этот параметр отключен, результат оценки сохраняется внутри системы."
|
setSensitiveFlagAutomaticallyDescription: "Даже если этот параметр отключен, результат оценки сохраняется внутри системы."
|
||||||
analyzeVideos: "Анализировать видео?"
|
analyzeVideos: "Анализировать видео?"
|
||||||
analyzeVideosDescription: "Анализируйте видео в дополнение к неподвижным изображениям. Нагрузка на сервер немного увеличивается."
|
analyzeVideosDescription: "Анализируйте видео в дополнение к неподвижным изображениям. Нагрузка на сервер немного увеличивается."
|
||||||
@ -1526,6 +1534,16 @@ _time:
|
|||||||
minute: "мин"
|
minute: "мин"
|
||||||
hour: "ч"
|
hour: "ч"
|
||||||
day: "сут"
|
day: "сут"
|
||||||
|
_timelineTutorial:
|
||||||
|
title: "Как пользоваться Misskey"
|
||||||
|
step1_1: "Это лицо Misskey, так называемая лента. Ваш инстанс, {name}, покажет тут все опубликованные на нём заметки в хронологическом порядке."
|
||||||
|
step1_2: "Здесь есть несколько лент. К примеру «персональная» лента отображает заметки тех, на кого вы подписаны. А «местная» — заметки тех, кого приютил {name}."
|
||||||
|
step2_1: "Что ж, теперь самое время опубликовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста."
|
||||||
|
step2_2: "Почему бы не написать немного о себе? Ну, или хотя бы «Привет, {name}»?"
|
||||||
|
step3_1: "Справились с первой заметкой?"
|
||||||
|
step3_2: "Отлично, теперь она должна появиться в вашей ленте."
|
||||||
|
step4_1: "А ещё здесь можно делиться своими реакциями на заметки."
|
||||||
|
step4_2: "Отмечайте реакции, нажимая на символ «+» под заметкой и выбирая значок по душе."
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
|
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
|
||||||
registerTOTP: "Начните настраивать приложение-аутентификатор"
|
registerTOTP: "Начните настраивать приложение-аутентификатор"
|
||||||
@ -1866,6 +1884,9 @@ _deck:
|
|||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
|
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
|
||||||
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
|
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
|
||||||
|
_disabledTimeline:
|
||||||
|
title: "Лента отключена"
|
||||||
|
description: "Ваша текущая роль не позволяет пользоваться этой лентой."
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Название"
|
name: "Название"
|
||||||
active: "Вкл."
|
active: "Вкл."
|
||||||
|
@ -560,6 +560,7 @@ accountDeletedDescription: "บัญชีนี้ถูกลบไปแล
|
|||||||
menu: "เมนู"
|
menu: "เมนู"
|
||||||
divider: "ตัวแบ่ง"
|
divider: "ตัวแบ่ง"
|
||||||
addItem: "เพิ่มรายการ"
|
addItem: "เพิ่มรายการ"
|
||||||
|
rearrange: "จัดใหม่"
|
||||||
relays: "รีเลย์"
|
relays: "รีเลย์"
|
||||||
addRelay: "เพิ่มรีเลย์"
|
addRelay: "เพิ่มรีเลย์"
|
||||||
inboxUrl: "อินบ็อกซ์ URL"
|
inboxUrl: "อินบ็อกซ์ URL"
|
||||||
@ -1030,6 +1031,7 @@ continue: "ดำเนินการต่อ"
|
|||||||
preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้"
|
preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้"
|
||||||
preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร"
|
preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร"
|
||||||
createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
|
createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
|
||||||
|
archive: "เก็บถาวร"
|
||||||
youFollowing: "ติดตามแล้ว"
|
youFollowing: "ติดตามแล้ว"
|
||||||
options: "ตัวเลือกบทบาท"
|
options: "ตัวเลือกบทบาท"
|
||||||
_serverRules:
|
_serverRules:
|
||||||
@ -1329,6 +1331,7 @@ _role:
|
|||||||
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
|
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
|
||||||
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
|
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
|
||||||
driveCapacity: "ความจุของไดรฟ์"
|
driveCapacity: "ความจุของไดรฟ์"
|
||||||
|
alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ"
|
||||||
pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้"
|
pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้"
|
||||||
antennaMax: "จำนวนสูงสุดของเสาอากาศ"
|
antennaMax: "จำนวนสูงสุดของเสาอากาศ"
|
||||||
wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ"
|
wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ"
|
||||||
@ -1580,6 +1583,12 @@ _time:
|
|||||||
minute: "นาที"
|
minute: "นาที"
|
||||||
hour: "ชั่วโมง"
|
hour: "ชั่วโมง"
|
||||||
day: "วัน"
|
day: "วัน"
|
||||||
|
_timelineTutorial:
|
||||||
|
title: "วิธีใช้งาน Misskey"
|
||||||
|
step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?"
|
||||||
|
step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ"
|
||||||
|
step4_1: "คุณยังสามารถแนบ \"ปฏิกิริยา\" ไปกับโน้ตได้อีกด้วยนะค่ะ"
|
||||||
|
step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว"
|
alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว"
|
||||||
registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์"
|
registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์"
|
||||||
|
@ -52,6 +52,8 @@ addToList: "添加至列表"
|
|||||||
sendMessage: "发送"
|
sendMessage: "发送"
|
||||||
copyRSS: "复制RSS"
|
copyRSS: "复制RSS"
|
||||||
copyUsername: "复制用户名"
|
copyUsername: "复制用户名"
|
||||||
|
copyUserId: "复制用户ID"
|
||||||
|
copyNoteId: "复制帖子ID"
|
||||||
searchUser: "搜索用户"
|
searchUser: "搜索用户"
|
||||||
reply: "回复"
|
reply: "回复"
|
||||||
loadMore: "查看更多"
|
loadMore: "查看更多"
|
||||||
@ -560,6 +562,7 @@ accountDeletedDescription: "此帐户已经被删除。"
|
|||||||
menu: "菜单"
|
menu: "菜单"
|
||||||
divider: "分割线"
|
divider: "分割线"
|
||||||
addItem: "添加项目"
|
addItem: "添加项目"
|
||||||
|
rearrange: "排序方式"
|
||||||
relays: "中继"
|
relays: "中继"
|
||||||
addRelay: "添加中继"
|
addRelay: "添加中继"
|
||||||
inboxUrl: "Inbox URL"
|
inboxUrl: "Inbox URL"
|
||||||
@ -789,6 +792,7 @@ noMaintainerInformationWarning: "管理人员信息未设置。"
|
|||||||
noBotProtectionWarning: "Bot保护未设置。"
|
noBotProtectionWarning: "Bot保护未设置。"
|
||||||
configure: "设置"
|
configure: "设置"
|
||||||
postToGallery: "发送到图库"
|
postToGallery: "发送到图库"
|
||||||
|
postToHashtag: "投稿到这个标签"
|
||||||
gallery: "图库"
|
gallery: "图库"
|
||||||
recentPosts: "最新发布"
|
recentPosts: "最新发布"
|
||||||
popularPosts: "热门投稿"
|
popularPosts: "热门投稿"
|
||||||
@ -822,6 +826,7 @@ translatedFrom: "从 {x} 翻译"
|
|||||||
accountDeletionInProgress: "正在删除账户"
|
accountDeletionInProgress: "正在删除账户"
|
||||||
usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。"
|
usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。"
|
||||||
aiChanMode: "小蓝模式"
|
aiChanMode: "小蓝模式"
|
||||||
|
devMode: "开发者模式"
|
||||||
keepCw: "回复时维持隐藏内容"
|
keepCw: "回复时维持隐藏内容"
|
||||||
pubSub: "Pub/Sub账户"
|
pubSub: "Pub/Sub账户"
|
||||||
lastCommunication: "最近通信"
|
lastCommunication: "最近通信"
|
||||||
@ -831,6 +836,8 @@ breakFollow: "移除关注者"
|
|||||||
breakFollowConfirm: "你想取消关注吗?"
|
breakFollowConfirm: "你想取消关注吗?"
|
||||||
itsOn: "已开启"
|
itsOn: "已开启"
|
||||||
itsOff: "已关闭"
|
itsOff: "已关闭"
|
||||||
|
on: "开启"
|
||||||
|
off: "关闭"
|
||||||
emailRequiredForSignup: "注册账户需要电子邮件地址"
|
emailRequiredForSignup: "注册账户需要电子邮件地址"
|
||||||
unread: "未读"
|
unread: "未读"
|
||||||
filter: "筛选"
|
filter: "筛选"
|
||||||
@ -985,10 +992,13 @@ cannotBeChangedLater: "之后不能再更改。"
|
|||||||
reactionAcceptance: "接受表情回应"
|
reactionAcceptance: "接受表情回应"
|
||||||
likeOnly: "仅点赞"
|
likeOnly: "仅点赞"
|
||||||
likeOnlyForRemote: "远程仅点赞"
|
likeOnlyForRemote: "远程仅点赞"
|
||||||
|
nonSensitiveOnly: "仅限非敏感内容"
|
||||||
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)"
|
||||||
rolesAssignedToMe: "指派给自己的角色"
|
rolesAssignedToMe: "指派给自己的角色"
|
||||||
resetPasswordConfirm: "确定重置密码?"
|
resetPasswordConfirm: "确定重置密码?"
|
||||||
sensitiveWords: "敏感词"
|
sensitiveWords: "敏感词"
|
||||||
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
|
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
|
||||||
|
sensitiveWordsDescription2: "用空格分割关键词作为AND格式,用斜线包裹关键字来构成正则表达式。"
|
||||||
notesSearchNotAvailable: "帖子检索不可用"
|
notesSearchNotAvailable: "帖子检索不可用"
|
||||||
license: "许可信息"
|
license: "许可信息"
|
||||||
unfavoriteConfirm: "确定要取消收藏吗?"
|
unfavoriteConfirm: "确定要取消收藏吗?"
|
||||||
@ -1028,28 +1038,60 @@ pleaseConfirmBelowBeforeSignup: "在这个服务器上注册账号前,请确
|
|||||||
pleaseAgreeAllToContinue: "必须全部勾选「同意」才能够继续。"
|
pleaseAgreeAllToContinue: "必须全部勾选「同意」才能够继续。"
|
||||||
continue: "继续"
|
continue: "继续"
|
||||||
preservedUsernames: "保留的用户名"
|
preservedUsernames: "保留的用户名"
|
||||||
|
preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
|
||||||
createNoteFromTheFile: "从文件创建帖子"
|
createNoteFromTheFile: "从文件创建帖子"
|
||||||
|
archive: "归档"
|
||||||
|
channelArchiveConfirmTitle: "要将{name}归档吗?"
|
||||||
|
channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。"
|
||||||
|
thisChannelArchived: "该频道已被归档。"
|
||||||
|
displayOfNote: "显示帖子"
|
||||||
|
initialAccountSetting: "初始设置"
|
||||||
youFollowing: "正在关注"
|
youFollowing: "正在关注"
|
||||||
|
preventAiLearning: "拒绝接受生成式AI的学习"
|
||||||
|
preventAiLearningDescription: "要求文章生成AI或图像生成AI不能够以发布的帖子和图像等内容作为学习对象。这是通过在HTML响应中包含noai标志来实现的,这不能完全阻止AI学习你的发布内容,并不是所有AI都会遵守这类请求。"
|
||||||
options: "选项"
|
options: "选项"
|
||||||
|
specifyUser: "用户指定"
|
||||||
|
failedToPreviewUrl: "无法预览"
|
||||||
|
update: "更新"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "在没有指定角色的情况下,任何人都可以使用表情作为回应。"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必须是公开的。"
|
||||||
|
cancelReactionConfirm: "要取消回应吗?"
|
||||||
|
changeReactionConfirm: "要更改回应吗?"
|
||||||
|
later: "一会再说"
|
||||||
|
goToMisskey: "去往Misskey"
|
||||||
|
installed: "已安装"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "账户创建完成了!"
|
accountCreated: "账户创建完成了!"
|
||||||
letsStartAccountSetup: "来进行帐户的初始设置吧。"
|
letsStartAccountSetup: "来进行帐户的初始设置吧。"
|
||||||
letsFillYourProfile: "首先,来设定你的个人档案吧!"
|
letsFillYourProfile: "首先,来设定你的个人档案吧!"
|
||||||
profileSetting: "个人资料设置"
|
profileSetting: "个人资料设置"
|
||||||
|
privacySetting: "隐私设置"
|
||||||
theseSettingsCanEditLater: "也可以在稍后修改这里的设置。"
|
theseSettingsCanEditLater: "也可以在稍后修改这里的设置。"
|
||||||
|
youCanEditMoreSettingsInSettingsPageLater: "还可以在「设置」页面进行其它各种设置,稍后就来确认一下看看吧。"
|
||||||
|
followUsers: "为了建立属于你自己的时间线,试着去关注你感兴趣的用户吧。"
|
||||||
|
pushNotificationDescription: "启用推送通知的话,就可以在设备上受到来自{name}的通知了。"
|
||||||
|
initialAccountSettingCompleted: "初始设定已经完成了!"
|
||||||
|
haveFun: "希望{name}在这里玩得开心!"
|
||||||
|
ifYouNeedLearnMore: "关于{name}(Misskey)的使用方法,详见{link}。"
|
||||||
|
skipAreYouSure: "要跳过初始设置吗?"
|
||||||
|
laterAreYouSure: "要稍后再进行初始设定吗?"
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。"
|
description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "从别的账号迁移到此账户"
|
moveFrom: "从别的账号迁移到此账户"
|
||||||
|
moveFromSub: "为另一个账户建立别名"
|
||||||
moveFromLabel: "迁移前的账户"
|
moveFromLabel: "迁移前的账户"
|
||||||
moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com"
|
moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com"
|
||||||
moveTo: "把这个账户迁移到新的账户"
|
moveTo: "把这个账户迁移到新的账户"
|
||||||
moveToLabel: "迁移后的账户"
|
moveToLabel: "迁移后的账户"
|
||||||
moveCannotBeUndone: "一旦迁移账户,就无法撤销。"
|
moveCannotBeUndone: "一旦迁移账户,就无法撤销。"
|
||||||
moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com"
|
moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com"
|
||||||
|
moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后,请像这样输入目标账户:@username@server.example.com"
|
||||||
startMigration: "迁移"
|
startMigration: "迁移"
|
||||||
migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。"
|
migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。"
|
||||||
movedAndCannotBeUndone: "该账户已被迁移。\n迁移操作无法撤销。"
|
movedAndCannotBeUndone: "该账户已被迁移。\n迁移操作无法撤销。"
|
||||||
|
postMigrationNote: "这个账户的关注会在迁移操作后的24小时后解除。该账户的「关注中」和「关注者」皆会变为0。由于不会解除关注关系,你的关注者仍然可以继续查看该账户发补给关注者的帖子。"
|
||||||
movedTo: "迁移后的账户"
|
movedTo: "迁移后的账户"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "达成时间"
|
earnedAt: "达成时间"
|
||||||
@ -1331,6 +1373,7 @@ _role:
|
|||||||
canInvite: "发放服务器邀请码"
|
canInvite: "发放服务器邀请码"
|
||||||
canManageCustomEmojis: "管理自定义表情符号"
|
canManageCustomEmojis: "管理自定义表情符号"
|
||||||
driveCapacity: "网盘容量"
|
driveCapacity: "网盘容量"
|
||||||
|
alwaysMarkNsfw: "总是将文件标记为NSFW"
|
||||||
pinMax: "帖子置顶数量限制"
|
pinMax: "帖子置顶数量限制"
|
||||||
antennaMax: "可创建的最大天线数量"
|
antennaMax: "可创建的最大天线数量"
|
||||||
wordMuteMax: "屏蔽词的字数限制"
|
wordMuteMax: "屏蔽词的字数限制"
|
||||||
@ -1583,8 +1626,15 @@ _time:
|
|||||||
hour: "小时"
|
hour: "小时"
|
||||||
day: "日"
|
day: "日"
|
||||||
_timelineTutorial:
|
_timelineTutorial:
|
||||||
|
title: "Misskey的使用方法"
|
||||||
|
step1_1: "这个画面是「时间线」。{name}的投稿会按照帖子的发布时间顺序来显示。"
|
||||||
|
step1_2: "时间线有许多种类,比如在「首页时间线」中展现的是你关注的人的贴文;而在「本地时间线」中展现的是{name}里全部用户的贴文。"
|
||||||
|
step2_1: "那么接下来,试着写一些什么东西来发布吧!你可以通过点击屏幕上的铅笔图标来打开投稿页面。"
|
||||||
|
step2_2: "第一次发布的帖子内容,建议包含自我介绍,以及「开始使用{name}了」。"
|
||||||
step3_1: "将想说的话发出去了吗?"
|
step3_1: "将想说的话发出去了吗?"
|
||||||
step3_2: "太棒了!现在你可以在你的时间线中看到刚刚发布的帖子了。"
|
step3_2: "太棒了!现在你可以在你的时间线中看到刚刚发布的帖子了。"
|
||||||
|
step4_1: "试着对帖子使用「回应」吧!"
|
||||||
|
step4_2: "在他人的帖子上按下「+」图标,即可选择想要的表情来进行「回应」。"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "此设备已被注册"
|
alreadyRegistered: "此设备已被注册"
|
||||||
registerTOTP: "开始设置认证应用"
|
registerTOTP: "开始设置认证应用"
|
||||||
|
@ -52,6 +52,8 @@ addToList: "加入至清單"
|
|||||||
sendMessage: "發送訊息"
|
sendMessage: "發送訊息"
|
||||||
copyRSS: "複製RSS"
|
copyRSS: "複製RSS"
|
||||||
copyUsername: "複製使用者名稱"
|
copyUsername: "複製使用者名稱"
|
||||||
|
copyUserId: "複製使用者ID"
|
||||||
|
copyNoteId: "複製貼文ID"
|
||||||
searchUser: "搜尋使用者"
|
searchUser: "搜尋使用者"
|
||||||
reply: "回覆"
|
reply: "回覆"
|
||||||
loadMore: "載入更多"
|
loadMore: "載入更多"
|
||||||
@ -790,6 +792,7 @@ noMaintainerInformationWarning: "尚未設定管理員信息。"
|
|||||||
noBotProtectionWarning: "尚未設定Bot防護。"
|
noBotProtectionWarning: "尚未設定Bot防護。"
|
||||||
configure: "設定"
|
configure: "設定"
|
||||||
postToGallery: "發佈到相簿"
|
postToGallery: "發佈到相簿"
|
||||||
|
postToHashtag: "以此主題標籤發布"
|
||||||
gallery: "相簿"
|
gallery: "相簿"
|
||||||
recentPosts: "最新貼文"
|
recentPosts: "最新貼文"
|
||||||
popularPosts: "熱門的貼文"
|
popularPosts: "熱門的貼文"
|
||||||
@ -823,6 +826,7 @@ translatedFrom: "從 {x} 翻譯"
|
|||||||
accountDeletionInProgress: "正在刪除帳戶"
|
accountDeletionInProgress: "正在刪除帳戶"
|
||||||
usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。"
|
usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。"
|
||||||
aiChanMode: "小藍模式"
|
aiChanMode: "小藍模式"
|
||||||
|
devMode: "開發者模式"
|
||||||
keepCw: "保持CW"
|
keepCw: "保持CW"
|
||||||
pubSub: "Pub/Sub 帳戶"
|
pubSub: "Pub/Sub 帳戶"
|
||||||
lastCommunication: "最近的通信"
|
lastCommunication: "最近的通信"
|
||||||
@ -832,6 +836,8 @@ breakFollow: "解除追隨者"
|
|||||||
breakFollowConfirm: "確定要取消被追隨嗎?"
|
breakFollowConfirm: "確定要取消被追隨嗎?"
|
||||||
itsOn: "已開啟"
|
itsOn: "已開啟"
|
||||||
itsOff: "已關閉"
|
itsOff: "已關閉"
|
||||||
|
on: "開啟"
|
||||||
|
off: "關閉"
|
||||||
emailRequiredForSignup: "註冊帳戶需要電子郵件地址"
|
emailRequiredForSignup: "註冊帳戶需要電子郵件地址"
|
||||||
unread: "未讀"
|
unread: "未讀"
|
||||||
filter: "篩選"
|
filter: "篩選"
|
||||||
@ -986,6 +992,8 @@ cannotBeChangedLater: "之後不能變更。"
|
|||||||
reactionAcceptance: "接受表情反應"
|
reactionAcceptance: "接受表情反應"
|
||||||
likeOnly: "僅限讚"
|
likeOnly: "僅限讚"
|
||||||
likeOnlyForRemote: "遠端僅限讚"
|
likeOnlyForRemote: "遠端僅限讚"
|
||||||
|
nonSensitiveOnly: "僅限非敏感"
|
||||||
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "僅限非敏感(遠端僅限按讚)"
|
||||||
rolesAssignedToMe: "指派給自己的角色"
|
rolesAssignedToMe: "指派給自己的角色"
|
||||||
resetPasswordConfirm: "重設密碼?"
|
resetPasswordConfirm: "重設密碼?"
|
||||||
sensitiveWords: "敏感詞"
|
sensitiveWords: "敏感詞"
|
||||||
@ -1039,14 +1047,27 @@ thisChannelArchived: "這個頻道已被封存。"
|
|||||||
displayOfNote: "顯示貼文"
|
displayOfNote: "顯示貼文"
|
||||||
initialAccountSetting: "初始設定"
|
initialAccountSetting: "初始設定"
|
||||||
youFollowing: "追隨中"
|
youFollowing: "追隨中"
|
||||||
preventAiLearning: "拒絕接受產生式AI的學習"
|
preventAiLearning: "拒絕接受生成式AI的訓練"
|
||||||
preventAiLearningDescription: "要求外部的文章產生AI或圖像產生AI不以發布的貼文和圖像等內容為學習對象。這是透過在HTML響應中包含noai旗標來實現的,但不能完全防止AI的學習,因為這要看該AI是否遵守這個要求。"
|
preventAiLearningDescription: "要求外部的文章生成式AI或圖像生成式AI不以發布的貼文和圖像等內容為學習對象。這是透過在HTML響應中包含noai旗標來實現的,但不能完全防止AI的學習,因為這要看該AI是否遵守這個要求。"
|
||||||
options: "選項"
|
options: "選項"
|
||||||
|
specifyUser: "指定使用者"
|
||||||
|
failedToPreviewUrl: "無法預覽"
|
||||||
|
update: "更新"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReaction: "可以當成反應使用的角色"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如果是未指定角色的情況,則任何人都可以被當成反應來使用。"
|
||||||
|
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必須是公開的角色。"
|
||||||
|
cancelReactionConfirm: "要取消做出的反應嗎?"
|
||||||
|
changeReactionConfirm: "要變更做出的反應嗎?"
|
||||||
|
later: "稍後再說"
|
||||||
|
goToMisskey: "往Misskey"
|
||||||
|
additionalEmojiDictionary: "表情符號的附加辭典"
|
||||||
|
installed: "已安裝"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "帳戶已建立完成!"
|
accountCreated: "帳戶已建立完成!"
|
||||||
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
|
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
|
||||||
letsFillYourProfile: "首先,來設定您的個人檔案吧。"
|
letsFillYourProfile: "首先,來設定您的個人檔案吧。"
|
||||||
profileSetting: "個人檔案設定"
|
profileSetting: "個人檔案設定"
|
||||||
|
privacySetting: "隱私設定"
|
||||||
theseSettingsCanEditLater: "這裡的設定可以在之後變更。"
|
theseSettingsCanEditLater: "這裡的設定可以在之後變更。"
|
||||||
youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。"
|
youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。"
|
||||||
followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。"
|
followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。"
|
||||||
@ -1055,6 +1076,7 @@ _initialAccountSetting:
|
|||||||
haveFun: "盡情享受{name}吧!"
|
haveFun: "盡情享受{name}吧!"
|
||||||
ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。"
|
ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。"
|
||||||
skipAreYouSure: "要略過初始設定嗎?"
|
skipAreYouSure: "要略過初始設定嗎?"
|
||||||
|
laterAreYouSure: "稍後再重新進行初始設定嗎?"
|
||||||
_serverRules:
|
_serverRules:
|
||||||
description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。"
|
description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
|
14
package.json
14
package.json
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.12.2",
|
"version": "13.13.1",
|
||||||
"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.3.1",
|
"packageManager": "pnpm@8.6.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
@ -51,16 +51,16 @@
|
|||||||
"gulp-replace": "1.1.4",
|
"gulp-replace": "1.1.4",
|
||||||
"gulp-terser": "2.1.0",
|
"gulp-terser": "2.1.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
"@typescript-eslint/eslint-plugin": "5.59.8",
|
||||||
"@typescript-eslint/parser": "5.59.5",
|
"@typescript-eslint/parser": "5.59.8",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.12.0",
|
"cypress": "12.13.0",
|
||||||
"eslint": "8.40.0",
|
"eslint": "8.41.0",
|
||||||
"start-server-and-test": "2.0.0"
|
"start-server-and-test": "2.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
13
packages/backend/migration/1683847157541-UserList.js
Normal file
13
packages/backend/migration/1683847157541-UserList.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export class UserList1683847157541 {
|
||||||
|
name = 'UserList1683847157541'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list" ADD "isPublic" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_48a00f08598662b9ca540521eb" ON "user_list" ("isPublic") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_48a00f08598662b9ca540521eb"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list" DROP COLUMN "isPublic"`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
export class UserListFavorites1683869758873 {
|
||||||
|
name = 'UserListFavorites1683869758873'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "user_list_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userListId" character varying(32) NOT NULL, CONSTRAINT "PK_c0974b21e18502a4c8178e09fe6" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_016f613dc4feb807e03e3e7da9" ON "user_list_favorite" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d6765a8c2a4c17c33f9d7f948b" ON "user_list_favorite" ("userId", "userListId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD CONSTRAINT "FK_016f613dc4feb807e03e3e7da92" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD CONSTRAINT "FK_4d52b20bfe32c8552e7a61e80d2" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP CONSTRAINT "FK_4d52b20bfe32c8552e7a61e80d2"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP CONSTRAINT "FK_016f613dc4feb807e03e3e7da92"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d6765a8c2a4c17c33f9d7f948b"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_016f613dc4feb807e03e3e7da9"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_list_favorite"`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
export class RemoveShowTimelineReplies1684206886988 {
|
||||||
|
name = 'RemoveShowTimelineReplies1684206886988'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showTimelineReplies"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "showTimelineReplies" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
}
|
15
packages/backend/migration/1684386446061-emoji-improve.js
Normal file
15
packages/backend/migration/1684386446061-emoji-improve.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export class EmojiImprove1684386446061 {
|
||||||
|
name = 'EmojiImprove1684386446061'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "emoji" ADD "localOnly" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "emoji" ADD "isSensitive" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "emoji" ADD "roleIdsThatCanBeUsedThisEmojiAsReaction" character varying(128) array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "roleIdsThatCanBeUsedThisEmojiAsReaction"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "isSensitive"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "localOnly"`);
|
||||||
|
}
|
||||||
|
}
|
@ -35,49 +35,52 @@
|
|||||||
"@swc/core-win32-x64-msvc": "1.3.56",
|
"@swc/core-win32-x64-msvc": "1.3.56",
|
||||||
"@tensorflow/tfjs": "4.4.0",
|
"@tensorflow/tfjs": "4.4.0",
|
||||||
"@tensorflow/tfjs-node": "4.4.0",
|
"@tensorflow/tfjs-node": "4.4.0",
|
||||||
"slacc-android-arm-eabi": "0.0.7",
|
"bufferutil": "^4.0.7",
|
||||||
"slacc-android-arm64": "0.0.7",
|
"slacc-android-arm-eabi": "0.0.9",
|
||||||
"slacc-darwin-arm64": "0.0.7",
|
"slacc-android-arm64": "0.0.9",
|
||||||
"slacc-darwin-universal": "0.0.7",
|
"slacc-darwin-arm64": "0.0.9",
|
||||||
"slacc-darwin-x64": "0.0.7",
|
"slacc-darwin-universal": "0.0.9",
|
||||||
"slacc-linux-arm-gnueabihf": "0.0.7",
|
"slacc-darwin-x64": "0.0.9",
|
||||||
"slacc-linux-arm64-gnu": "0.0.7",
|
"slacc-freebsd-x64": "0.0.9",
|
||||||
"slacc-linux-arm64-musl": "0.0.7",
|
"slacc-linux-arm-gnueabihf": "0.0.9",
|
||||||
"slacc-linux-x64-gnu": "0.0.7",
|
"slacc-linux-arm64-gnu": "0.0.9",
|
||||||
"slacc-win32-arm64-msvc": "0.0.7",
|
"slacc-linux-arm64-musl": "0.0.9",
|
||||||
"slacc-win32-x64-msvc": "0.0.7"
|
"slacc-linux-x64-gnu": "0.0.9",
|
||||||
|
"slacc-win32-arm64-msvc": "0.0.9",
|
||||||
|
"slacc-win32-x64-msvc": "0.0.9",
|
||||||
|
"utf-8-validate": "^6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.321.1",
|
"@aws-sdk/client-s3": "3.321.1",
|
||||||
"@aws-sdk/lib-storage": "3.321.1",
|
"@aws-sdk/lib-storage": "3.321.1",
|
||||||
"@aws-sdk/node-http-handler": "3.321.1",
|
"@aws-sdk/node-http-handler": "3.321.1",
|
||||||
"@bull-board/api": "5.1.2",
|
"@bull-board/api": "5.2.0",
|
||||||
"@bull-board/fastify": "5.1.2",
|
"@bull-board/fastify": "5.2.0",
|
||||||
"@bull-board/ui": "5.1.2",
|
"@bull-board/ui": "5.2.0",
|
||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@fastify/accepts": "4.1.0",
|
"@fastify/accepts": "4.1.0",
|
||||||
"@fastify/cookie": "8.3.0",
|
"@fastify/cookie": "8.3.0",
|
||||||
"@fastify/cors": "8.2.1",
|
"@fastify/cors": "8.3.0",
|
||||||
"@fastify/http-proxy": "9.1.0",
|
"@fastify/http-proxy": "9.1.0",
|
||||||
"@fastify/multipart": "7.6.0",
|
"@fastify/multipart": "7.6.0",
|
||||||
"@fastify/static": "6.10.1",
|
"@fastify/static": "6.10.2",
|
||||||
"@fastify/view": "7.4.1",
|
"@fastify/view": "7.4.1",
|
||||||
"@nestjs/common": "9.4.0",
|
"@nestjs/common": "9.4.2",
|
||||||
"@nestjs/core": "9.4.0",
|
"@nestjs/core": "9.4.2",
|
||||||
"@nestjs/testing": "9.4.0",
|
"@nestjs/testing": "9.4.2",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sinonjs/fake-timers": "10.0.2",
|
"@sinonjs/fake-timers": "10.2.0",
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.56",
|
"@swc/core": "1.3.61",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"bull": "4.10.4",
|
"bullmq": "3.15.0",
|
||||||
"cacheable-lookup": "6.1.0",
|
"cacheable-lookup": "6.1.0",
|
||||||
"cbor": "8.1.0",
|
"cbor": "9.0.0",
|
||||||
"chalk": "5.2.0",
|
"chalk": "5.2.0",
|
||||||
"chalk-template": "0.4.0",
|
"chalk-template": "0.4.0",
|
||||||
"chokidar": "3.5.3",
|
"chokidar": "3.5.3",
|
||||||
@ -93,30 +96,30 @@
|
|||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "12.6.0",
|
"got": "12.6.0",
|
||||||
"happy-dom": "9.16.0",
|
"happy-dom": "9.20.3",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.3.2",
|
||||||
"ip-cidr": "3.1.0",
|
"ip-cidr": "3.1.0",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "21.1.1",
|
"jsdom": "22.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.1.1",
|
"jsonld": "8.2.0",
|
||||||
"meilisearch": "0.32.3",
|
|
||||||
"jsrsasign": "10.8.6",
|
"jsrsasign": "10.8.6",
|
||||||
|
"meilisearch": "0.32.5",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"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",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.1",
|
"node-fetch": "3.3.1",
|
||||||
"nodemailer": "6.9.2",
|
"nodemailer": "6.9.3",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.1.2",
|
"otpauth": "9.1.2",
|
||||||
"parse5": "7.1.2",
|
"parse5": "7.1.2",
|
||||||
"pg": "8.10.0",
|
"pg": "8.11.0",
|
||||||
"private-ip": "3.0.0",
|
"private-ip": "3.0.0",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
@ -126,7 +129,7 @@
|
|||||||
"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.18.0",
|
"re2": "1.19.0",
|
||||||
"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",
|
||||||
@ -136,27 +139,26 @@
|
|||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sanitize-html": "2.10.0",
|
"sanitize-html": "2.10.0",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
"semver": "7.5.0",
|
"semver": "7.5.1",
|
||||||
"sharp": "0.32.1",
|
"sharp": "0.32.1",
|
||||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||||
"slacc": "0.0.7",
|
"slacc": "0.0.9",
|
||||||
"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.17.12",
|
"systeminformation": "5.17.16",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.6",
|
"tsc-alias": "1.8.6",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.16",
|
"typeorm": "0.3.16",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.1.3",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"unzipper": "0.10.11",
|
"unzipper": "0.10.14",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.6.1",
|
"web-push": "3.6.1",
|
||||||
"websocket": "1.0.34",
|
|
||||||
"ws": "8.13.0",
|
"ws": "8.13.0",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
@ -166,23 +168,22 @@
|
|||||||
"@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.2",
|
||||||
"@types/bull": "4.10.0",
|
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
"@types/color-convert": "2.0.0",
|
"@types/color-convert": "2.0.0",
|
||||||
"@types/content-disposition": "0.5.5",
|
"@types/content-disposition": "0.5.5",
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/fluent-ffmpeg": "2.1.21",
|
"@types/fluent-ffmpeg": "2.1.21",
|
||||||
"@types/jest": "29.5.1",
|
"@types/jest": "29.5.2",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jsdom": "21.1.1",
|
"@types/jsdom": "21.1.1",
|
||||||
"@types/jsonld": "1.5.8",
|
"@types/jsonld": "1.5.8",
|
||||||
"@types/jsrsasign": "10.5.8",
|
"@types/jsrsasign": "10.5.8",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.1",
|
||||||
"@types/node": "20.1.3",
|
"@types/node": "20.2.5",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.7",
|
"@types/nodemailer": "6.4.8",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
"@types/pg": "8.6.6",
|
"@types/pg": "8.10.1",
|
||||||
"@types/pug": "2.0.6",
|
"@types/pug": "2.0.6",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
"@types/qrcode": "1.5.0",
|
"@types/qrcode": "1.5.0",
|
||||||
@ -196,17 +197,17 @@
|
|||||||
"@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/unzipper": "0.10.5",
|
"@types/unzipper": "0.10.6",
|
||||||
"@types/uuid": "9.0.1",
|
"@types/uuid": "9.0.1",
|
||||||
"@types/vary": "1.1.0",
|
"@types/vary": "1.1.0",
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.4",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
"@typescript-eslint/eslint-plugin": "5.59.8",
|
||||||
"@typescript-eslint/parser": "5.59.5",
|
"@typescript-eslint/parser": "5.59.8",
|
||||||
"aws-sdk-client-mock": "2.1.1",
|
"aws-sdk-client-mock": "2.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.40.0",
|
"eslint": "8.41.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
|
@ -4,7 +4,7 @@ import * as Redis from 'ioredis';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { MeiliSearch } from 'meilisearch';
|
import { MeiliSearch } from 'meilisearch';
|
||||||
import { DI } from './di-symbols.js';
|
import { DI } from './di-symbols.js';
|
||||||
import { loadConfig } from './config.js';
|
import { Config, loadConfig } from './config.js';
|
||||||
import { createPostgresDataSource } from './postgres.js';
|
import { createPostgresDataSource } from './postgres.js';
|
||||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||||
@ -25,7 +25,7 @@ const $db: Provider = {
|
|||||||
|
|
||||||
const $meilisearch: Provider = {
|
const $meilisearch: Provider = {
|
||||||
provide: DI.meilisearch,
|
provide: DI.meilisearch,
|
||||||
useFactory: (config) => {
|
useFactory: (config: Config) => {
|
||||||
if (config.meilisearch) {
|
if (config.meilisearch) {
|
||||||
return new MeiliSearch({
|
return new MeiliSearch({
|
||||||
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
|
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||||
@ -40,7 +40,7 @@ const $meilisearch: Provider = {
|
|||||||
|
|
||||||
const $redis: Provider = {
|
const $redis: Provider = {
|
||||||
provide: DI.redis,
|
provide: DI.redis,
|
||||||
useFactory: (config) => {
|
useFactory: (config: Config) => {
|
||||||
return new Redis.Redis({
|
return new Redis.Redis({
|
||||||
port: config.redis.port,
|
port: config.redis.port,
|
||||||
host: config.redis.host,
|
host: config.redis.host,
|
||||||
@ -55,7 +55,7 @@ const $redis: Provider = {
|
|||||||
|
|
||||||
const $redisForPub: Provider = {
|
const $redisForPub: Provider = {
|
||||||
provide: DI.redisForPub,
|
provide: DI.redisForPub,
|
||||||
useFactory: (config) => {
|
useFactory: (config: Config) => {
|
||||||
const redis = new Redis.Redis({
|
const redis = new Redis.Redis({
|
||||||
port: config.redisForPubsub.port,
|
port: config.redisForPubsub.port,
|
||||||
host: config.redisForPubsub.host,
|
host: config.redisForPubsub.host,
|
||||||
@ -71,7 +71,7 @@ const $redisForPub: Provider = {
|
|||||||
|
|
||||||
const $redisForSub: Provider = {
|
const $redisForSub: Provider = {
|
||||||
provide: DI.redisForSub,
|
provide: DI.redisForSub,
|
||||||
useFactory: (config) => {
|
useFactory: (config: Config) => {
|
||||||
const redis = new Redis.Redis({
|
const redis = new Redis.Redis({
|
||||||
port: config.redisForPubsub.port,
|
port: config.redisForPubsub.port,
|
||||||
host: config.redisForPubsub.host,
|
host: config.redisForPubsub.host,
|
||||||
@ -100,7 +100,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onApplicationShutdown(signal: string): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
// XXX:
|
// XXX:
|
||||||
// Shutting down the existing connections causes errors on Jest as
|
// Shutting down the existing connections causes errors on Jest as
|
||||||
@ -116,4 +116,8 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||||||
this.redisForSub.disconnect(),
|
this.redisForSub.disconnect(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onApplicationShutdown(signal: string): Promise<void> {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,7 @@ export function loadConfig() {
|
|||||||
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
|
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
|
||||||
const clientManifest = clientManifestExists ?
|
const clientManifest = clientManifestExists ?
|
||||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
|
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
|
||||||
: { 'src/init.ts': { file: 'src/init.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 mixin = {} as Mixin;
|
||||||
@ -165,7 +165,7 @@ export function loadConfig() {
|
|||||||
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
|
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
|
||||||
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
|
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
|
||||||
mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
|
mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
|
||||||
mixin.clientEntry = clientManifest['src/init.ts'];
|
mixin.clientEntry = clientManifest['src/_boot_.ts'];
|
||||||
mixin.clientManifestExists = clientManifestExists;
|
mixin.clientManifestExists = clientManifestExists;
|
||||||
|
|
||||||
const externalMediaProxy = config.mediaProxy ?
|
const externalMediaProxy = config.mediaProxy ?
|
||||||
@ -190,6 +190,6 @@ function tryCreateUrl(url: string) {
|
|||||||
try {
|
try {
|
||||||
return new URL(url);
|
return new URL(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw `url="${url}" is not a valid URL.`;
|
throw new Error(`url="${url}" is not a valid URL.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,11 +55,6 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
this.redisForSub.on('message', this.onRedisMessage);
|
this.redisForSub.on('message', this.onRedisMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
|
||||||
this.redisForSub.off('message', this.onRedisMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onRedisMessage(_: string, data: string): Promise<void> {
|
private async onRedisMessage(_: string, data: string): Promise<void> {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
@ -196,4 +191,14 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
return this.antennas;
|
return this.antennas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onRedisMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,7 +166,12 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export class CaptchaService {
|
|||||||
}, { throwErrorWhenResponseNotOk: false });
|
}, { throwErrorWhenResponseNotOk: false });
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw `${res.status}`;
|
throw new Error(`${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await res.json() as CaptchaResponse;
|
return await res.json() as CaptchaResponse;
|
||||||
@ -39,48 +39,48 @@ export class CaptchaService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
throw 'recaptcha-failed: no response provided';
|
throw new Error('recaptcha-failed: no response provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => {
|
const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => {
|
||||||
throw `recaptcha-request-failed: ${err}`;
|
throw new Error(`recaptcha-request-failed: ${err}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success !== true) {
|
if (result.success !== true) {
|
||||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||||
throw `recaptcha-failed: ${errorCodes}`;
|
throw new Error(`recaptcha-failed: ${errorCodes}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
throw 'hcaptcha-failed: no response provided';
|
throw new Error('hcaptcha-failed: no response provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
|
const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
|
||||||
throw `hcaptcha-request-failed: ${err}`;
|
throw new Error(`hcaptcha-request-failed: ${err}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success !== true) {
|
if (result.success !== true) {
|
||||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||||
throw `hcaptcha-failed: ${errorCodes}`;
|
throw new Error(`hcaptcha-failed: ${errorCodes}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
throw 'turnstile-failed: no response provided';
|
throw new Error('turnstile-failed: no response provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
|
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
|
||||||
throw `turnstile-request-failed: ${err}`;
|
throw new Error(`turnstile-request-failed: ${err}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success !== true) {
|
if (result.success !== true) {
|
||||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||||
throw `turnstile-failed: ${errorCodes}`;
|
throw new Error(`turnstile-failed: ${errorCodes}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository, Role } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
@ -15,6 +15,8 @@ import type { Config } from '@/config.js';
|
|||||||
import { query } from '@/misc/prelude/url.js';
|
import { query } from '@/misc/prelude/url.js';
|
||||||
import type { Serialized } from '@/server/api/stream/types.js';
|
import type { Serialized } from '@/server/api/stream/types.js';
|
||||||
|
|
||||||
|
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomEmojiService {
|
export class CustomEmojiService {
|
||||||
private cache: MemoryKVCache<Emoji | null>;
|
private cache: MemoryKVCache<Emoji | null>;
|
||||||
@ -63,6 +65,9 @@ export class CustomEmojiService {
|
|||||||
aliases: string[];
|
aliases: string[];
|
||||||
host: string | null;
|
host: string | null;
|
||||||
license: string | null;
|
license: string | null;
|
||||||
|
isSensitive: boolean;
|
||||||
|
localOnly: boolean;
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][];
|
||||||
}): Promise<Emoji> {
|
}): Promise<Emoji> {
|
||||||
const emoji = await this.emojisRepository.insert({
|
const emoji = await this.emojisRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
@ -75,6 +80,9 @@ export class CustomEmojiService {
|
|||||||
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
|
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
|
||||||
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
||||||
license: data.license,
|
license: data.license,
|
||||||
|
isSensitive: data.isSensitive,
|
||||||
|
localOnly: data.localOnly,
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
if (data.host == null) {
|
if (data.host == null) {
|
||||||
@ -90,10 +98,14 @@ export class CustomEmojiService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async update(id: Emoji['id'], data: {
|
public async update(id: Emoji['id'], data: {
|
||||||
|
driveFile?: DriveFile;
|
||||||
name?: string;
|
name?: string;
|
||||||
category?: string | null;
|
category?: string | null;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
license?: string | null;
|
license?: string | null;
|
||||||
|
isSensitive?: boolean;
|
||||||
|
localOnly?: boolean;
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||||
@ -105,6 +117,12 @@ export class CustomEmojiService {
|
|||||||
category: data.category,
|
category: data.category,
|
||||||
aliases: data.aliases,
|
aliases: data.aliases,
|
||||||
license: data.license,
|
license: data.license,
|
||||||
|
isSensitive: data.isSensitive,
|
||||||
|
localOnly: data.localOnly,
|
||||||
|
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
|
||||||
|
publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined,
|
||||||
|
type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.localEmojisCache.refresh();
|
this.localEmojisCache.refresh();
|
||||||
@ -259,7 +277,7 @@ export class CustomEmojiService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
const match = emojiName.match(parseEmojiStrRegexp);
|
||||||
if (!match) return { name: null, host: null };
|
if (!match) return { name: null, host: null };
|
||||||
|
|
||||||
const name = match[1];
|
const name = match[1];
|
||||||
|
@ -116,14 +116,14 @@ export class FetchInstanceMetadataService {
|
|||||||
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.statusCode === 404) {
|
if (err.statusCode === 404) {
|
||||||
throw 'No nodeinfo provided';
|
throw new Error('No nodeinfo provided');
|
||||||
} else {
|
} else {
|
||||||
throw err.statusCode ?? err.message;
|
throw err.statusCode ?? err.message;
|
||||||
}
|
}
|
||||||
}) as Record<string, unknown>;
|
}) as Record<string, unknown>;
|
||||||
|
|
||||||
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
||||||
throw 'No wellknown links';
|
throw new Error('No wellknown links');
|
||||||
}
|
}
|
||||||
|
|
||||||
const links = wellknown.links as any[];
|
const links = wellknown.links as any[];
|
||||||
@ -134,7 +134,7 @@ export class FetchInstanceMetadataService {
|
|||||||
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
|
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
|
||||||
|
|
||||||
if (link == null) {
|
if (link == null) {
|
||||||
throw 'No nodeinfo link provided';
|
throw new Error('No nodeinfo link provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await this.httpRequestService.getJson(link.href)
|
const info = await this.httpRequestService.getJson(link.href)
|
||||||
|
@ -120,8 +120,13 @@ export class MetaService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public dispose(): void {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ export class MfmService {
|
|||||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||||
text += txt;
|
text += txt;
|
||||||
// メンション
|
// メンション
|
||||||
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||||
const part = txt.split('@');
|
const part = txt.split('@');
|
||||||
|
|
||||||
if (part.length === 2 && href) {
|
if (part.length === 2 && href) {
|
||||||
|
@ -510,7 +510,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
if (data.poll && data.poll.expiresAt) {
|
if (data.poll && data.poll.expiresAt) {
|
||||||
const delay = data.poll.expiresAt.getTime() - Date.now();
|
const delay = data.poll.expiresAt.getTime() - Date.now();
|
||||||
this.queueService.endedPollNotificationQueue.add({
|
this.queueService.endedPollNotificationQueue.add(note.id, {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, {
|
}, {
|
||||||
delay,
|
delay,
|
||||||
@ -790,7 +790,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
return mentionedUsers;
|
return mentionedUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
onApplicationShutdown(signal?: string | undefined) {
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
this.#shutdownController.abort();
|
this.#shutdownController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,13 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onApplicationShutdown(signal?: string | undefined): void {
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
this.#shutdownController.abort();
|
this.#shutdownController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,13 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
onApplicationShutdown(signal?: string | undefined): void {
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
this.#shutdownController.abort();
|
this.#shutdownController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,7 +208,7 @@ export class QueryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null): void {
|
public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<User, 'id'> | null): void {
|
||||||
if (me == null) {
|
if (me == null) {
|
||||||
q.andWhere(new Brackets(qb => { qb
|
q.andWhere(new Brackets(qb => { qb
|
||||||
.where('note.replyId IS NULL') // 返信ではない
|
.where('note.replyId IS NULL') // 返信ではない
|
||||||
@ -217,7 +217,7 @@ export class QueryService {
|
|||||||
.andWhere('note.replyUserId = note.userId');
|
.andWhere('note.replyUserId = note.userId');
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
} else if (!me.showTimelineReplies) {
|
} else if (!withReplies) {
|
||||||
q.andWhere(new Brackets(qb => { qb
|
q.andWhere(new Brackets(qb => { qb
|
||||||
.where('note.replyId IS NULL') // 返信ではない
|
.where('note.replyId IS NULL') // 返信ではない
|
||||||
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
|
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
|
||||||
|
@ -1,42 +1,11 @@
|
|||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import Bull from 'bull';
|
import * as Bull from 'bullmq';
|
||||||
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 { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js';
|
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
||||||
|
|
||||||
function q<T>(config: Config, name: string, limitPerSec = -1) {
|
|
||||||
return new Bull<T>(name, {
|
|
||||||
redis: {
|
|
||||||
port: config.redisForJobQueue.port,
|
|
||||||
host: config.redisForJobQueue.host,
|
|
||||||
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
|
|
||||||
password: config.redisForJobQueue.pass,
|
|
||||||
db: config.redisForJobQueue.db ?? 0,
|
|
||||||
},
|
|
||||||
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
|
|
||||||
limiter: limitPerSec > 0 ? {
|
|
||||||
max: limitPerSec,
|
|
||||||
duration: 1000,
|
|
||||||
} : undefined,
|
|
||||||
settings: {
|
|
||||||
backoffStrategies: {
|
|
||||||
apBackoff,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
|
||||||
function apBackoff(attemptsMade: number, err: Error) {
|
|
||||||
const baseDelay = 60 * 1000; // 1min
|
|
||||||
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
|
|
||||||
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
|
||||||
backoff = Math.min(backoff, maxBackoff);
|
|
||||||
backoff += Math.round(backoff * Math.random() * 0.2);
|
|
||||||
return backoff;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||||
@ -49,49 +18,49 @@ export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
|
|||||||
|
|
||||||
const $system: Provider = {
|
const $system: Provider = {
|
||||||
provide: 'queue:system',
|
provide: 'queue:system',
|
||||||
useFactory: (config: Config) => q(config, 'system'),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM, baseQueueOptions(config, QUEUE.SYSTEM)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $endedPollNotification: Provider = {
|
const $endedPollNotification: Provider = {
|
||||||
provide: 'queue:endedPollNotification',
|
provide: 'queue:endedPollNotification',
|
||||||
useFactory: (config: Config) => q(config, 'endedPollNotification'),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.ENDED_POLL_NOTIFICATION, baseQueueOptions(config, QUEUE.ENDED_POLL_NOTIFICATION)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $deliver: Provider = {
|
const $deliver: Provider = {
|
||||||
provide: 'queue:deliver',
|
provide: 'queue:deliver',
|
||||||
useFactory: (config: Config) => q(config, 'deliver', config.deliverJobPerSec ?? 128),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $inbox: Provider = {
|
const $inbox: Provider = {
|
||||||
provide: 'queue:inbox',
|
provide: 'queue:inbox',
|
||||||
useFactory: (config: Config) => q(config, 'inbox', config.inboxJobPerSec ?? 16),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config, QUEUE.INBOX)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $db: Provider = {
|
const $db: Provider = {
|
||||||
provide: 'queue:db',
|
provide: 'queue:db',
|
||||||
useFactory: (config: Config) => q(config, 'db'),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.DB, baseQueueOptions(config, QUEUE.DB)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $relationship: Provider = {
|
const $relationship: Provider = {
|
||||||
provide: 'queue:relationship',
|
provide: 'queue:relationship',
|
||||||
useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config, QUEUE.RELATIONSHIP)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $objectStorage: Provider = {
|
const $objectStorage: Provider = {
|
||||||
provide: 'queue:objectStorage',
|
provide: 'queue:objectStorage',
|
||||||
useFactory: (config: Config) => q(config, 'objectStorage'),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.OBJECT_STORAGE, baseQueueOptions(config, QUEUE.OBJECT_STORAGE)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $webhookDeliver: Provider = {
|
const $webhookDeliver: Provider = {
|
||||||
provide: 'queue:webhookDeliver',
|
provide: 'queue:webhookDeliver',
|
||||||
useFactory: (config: Config) => q(config, 'webhookDeliver', 64),
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -131,7 +100,7 @@ export class QueueModule implements OnApplicationShutdown {
|
|||||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onApplicationShutdown(signal: string): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
// XXX:
|
// XXX:
|
||||||
// Shutting down the existing connections causes errors on Jest as
|
// Shutting down the existing connections causes errors on Jest as
|
||||||
@ -151,4 +120,8 @@ export class QueueModule implements OnApplicationShutdown {
|
|||||||
this.webhookDeliverQueue.close(),
|
this.webhookDeliverQueue.close(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onApplicationShutdown(signal: string): Promise<void> {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import Bull from 'bull';
|
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
||||||
@ -11,6 +10,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
|||||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||||
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
@ -26,7 +26,43 @@ export class QueueService {
|
|||||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||||
) {}
|
) {
|
||||||
|
this.systemQueue.add('tickCharts', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '55 * * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('resyncCharts', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '0 0 * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('cleanCharts', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '0 0 * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('aggregateRetention', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '0 0 * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('clean', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '0 0 * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('checkExpiredMutings', {
|
||||||
|
}, {
|
||||||
|
repeat: { pattern: '*/5 * * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
|
public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
|
||||||
@ -42,11 +78,10 @@ export class QueueService {
|
|||||||
isSharedInbox,
|
isSharedInbox,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.deliverQueue.add(data, {
|
return this.deliverQueue.add(to, data, {
|
||||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||||
timeout: 1 * 60 * 1000, // 1min
|
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'apBackoff',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
@ -60,11 +95,10 @@ export class QueueService {
|
|||||||
signature,
|
signature,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.inboxQueue.add(data, {
|
return this.inboxQueue.add('', data, {
|
||||||
attempts: this.config.inboxJobMaxAttempts ?? 8,
|
attempts: this.config.inboxJobMaxAttempts ?? 8,
|
||||||
timeout: 5 * 60 * 1000, // 5min
|
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'apBackoff',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
@ -212,7 +246,7 @@ export class QueueService {
|
|||||||
private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): {
|
private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): {
|
||||||
name: string,
|
name: string,
|
||||||
data: D,
|
data: D,
|
||||||
opts: Bull.JobOptions,
|
opts: Bull.JobsOptions,
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@ -299,10 +333,10 @@ export class QueueService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): {
|
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobsOptions = {}): {
|
||||||
name: string,
|
name: string,
|
||||||
data: RelationshipJobData,
|
data: RelationshipJobData,
|
||||||
opts: Bull.JobOptions,
|
opts: Bull.JobsOptions,
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@ -351,11 +385,10 @@ export class QueueService {
|
|||||||
eventId: uuid(),
|
eventId: uuid(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.webhookDeliverQueue.add(data, {
|
return this.webhookDeliverQueue.add(webhook.id, data, {
|
||||||
attempts: 4,
|
attempts: 4,
|
||||||
timeout: 1 * 60 * 1000, // 1min
|
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'apBackoff',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
@ -367,11 +400,11 @@ export class QueueService {
|
|||||||
this.deliverQueue.once('cleaned', (jobs, status) => {
|
this.deliverQueue.once('cleaned', (jobs, status) => {
|
||||||
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||||
});
|
});
|
||||||
this.deliverQueue.clean(0, 'delayed');
|
this.deliverQueue.clean(0, Infinity, 'delayed');
|
||||||
|
|
||||||
this.inboxQueue.once('cleaned', (jobs, status) => {
|
this.inboxQueue.once('cleaned', (jobs, status) => {
|
||||||
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||||
});
|
});
|
||||||
this.inboxQueue.clean(0, 'delayed');
|
this.inboxQueue.clean(0, Infinity, 'delayed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
|
||||||
const FALLBACK = '❤';
|
const FALLBACK = '❤';
|
||||||
|
|
||||||
@ -54,6 +55,9 @@ type DecodedReaction = {
|
|||||||
host?: string | null;
|
host?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/;
|
||||||
|
const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReactionService {
|
export class ReactionService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -72,6 +76,7 @@ export class ReactionService {
|
|||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private customEmojiService: CustomEmojiService,
|
private customEmojiService: CustomEmojiService,
|
||||||
|
private roleService: RoleService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
@ -85,7 +90,7 @@ export class ReactionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) {
|
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, _reaction?: string | null) {
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (note.userId !== user.id) {
|
if (note.userId !== user.id) {
|
||||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||||
@ -99,10 +104,41 @@ export class ReactionService {
|
|||||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
let reaction = _reaction ?? FALLBACK;
|
||||||
|
|
||||||
|
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
||||||
reaction = '❤️';
|
reaction = '❤️';
|
||||||
|
} else if (_reaction) {
|
||||||
|
const custom = reaction.match(isCustomEmojiRegexp);
|
||||||
|
if (custom) {
|
||||||
|
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
||||||
|
|
||||||
|
const name = custom[1];
|
||||||
|
const emoji = reacterHost == null
|
||||||
|
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||||
|
: await this.emojisRepository.findOneBy({
|
||||||
|
host: reacterHost,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (emoji) {
|
||||||
|
if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) {
|
||||||
|
reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||||
|
|
||||||
|
// センシティブ
|
||||||
|
if ((note.reactionAcceptance === 'nonSensitiveOnly') && emoji.isSensitive) {
|
||||||
|
reaction = FALLBACK;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
reaction = await this.toDbReaction(reaction, user.host);
|
// リアクションとして使う権限がない
|
||||||
|
reaction = FALLBACK;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reaction = FALLBACK;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reaction = this.normalize(reaction ?? null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const record: NoteReaction = {
|
const record: NoteReaction = {
|
||||||
@ -288,11 +324,9 @@ export class ReactionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
|
public normalize(reaction: string | null): string {
|
||||||
if (reaction == null) return FALLBACK;
|
if (reaction == null) return FALLBACK;
|
||||||
|
|
||||||
reacterHost = this.utilityService.toPunyNullable(reacterHost);
|
|
||||||
|
|
||||||
// 文字列タイプのリアクションを絵文字に変換
|
// 文字列タイプのリアクションを絵文字に変換
|
||||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||||
|
|
||||||
@ -306,25 +340,12 @@ export class ReactionService {
|
|||||||
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
|
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
|
||||||
if (custom) {
|
|
||||||
const name = custom[1];
|
|
||||||
const emoji = reacterHost == null
|
|
||||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
|
||||||
: await this.emojisRepository.findOneBy({
|
|
||||||
host: reacterHost,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return FALLBACK;
|
return FALLBACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public decodeReaction(str: string): DecodedReaction {
|
public decodeReaction(str: string): DecodedReaction {
|
||||||
const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
|
const custom = str.match(decodeCustomEmojiRegexp);
|
||||||
|
|
||||||
if (custom) {
|
if (custom) {
|
||||||
const name = custom[1];
|
const name = custom[1];
|
||||||
|
@ -306,6 +306,14 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
|
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async isExplorable(role: { id: Role['id']} | null): Promise<boolean> {
|
||||||
|
if (role == null) return false;
|
||||||
|
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
||||||
|
if (check == null) return false;
|
||||||
|
return check.isExplorable;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
||||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||||
@ -425,7 +433,12 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,9 @@ type IWebFinger = {
|
|||||||
subject: string;
|
subject: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const urlRegex = /^https?:\/\//;
|
||||||
|
const mRegex = /^([^@]+)@(.*)/;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebfingerService {
|
export class WebfingerService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -35,12 +38,12 @@ export class WebfingerService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private genUrl(query: string): string {
|
private genUrl(query: string): string {
|
||||||
if (query.match(/^https?:\/\//)) {
|
if (query.match(urlRegex)) {
|
||||||
const u = new URL(query);
|
const u = new URL(query);
|
||||||
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
|
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
|
||||||
}
|
}
|
||||||
|
|
||||||
const m = query.match(/^([^@]+)@(.*)/);
|
const m = query.match(mRegex);
|
||||||
if (m) {
|
if (m) {
|
||||||
const hostname = m[2];
|
const hostname = m[2];
|
||||||
const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
|
const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
|
||||||
|
@ -81,7 +81,12 @@ export class WebhookService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,7 +277,7 @@ export class ApRendererService {
|
|||||||
const name = reaction.replaceAll(':', '');
|
const name = reaction.replaceAll(':', '');
|
||||||
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||||
|
|
||||||
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
if (emoji && !emoji.localOnly) object.tag = [this.renderEmoji(emoji)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
@ -400,7 +400,7 @@ export class ApRendererService {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const emojis = await this.getEmojis(note.emojis);
|
const emojis = await this.getEmojis(note.emojis);
|
||||||
const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
|
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||||
|
|
||||||
const tag = [
|
const tag = [
|
||||||
...hashtagTags,
|
...hashtagTags,
|
||||||
@ -479,7 +479,7 @@ export class ApRendererService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const emojis = await this.getEmojis(user.emojis);
|
const emojis = await this.getEmojis(user.emojis);
|
||||||
const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
|
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||||
|
|
||||||
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag));
|
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag));
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ class LdSignature {
|
|||||||
@bindThis
|
@bindThis
|
||||||
private getLoader() {
|
private getLoader() {
|
||||||
return async (url: string): Promise<any> => {
|
return async (url: string): Promise<any> => {
|
||||||
if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`;
|
if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`);
|
||||||
|
|
||||||
if (this.preLoad) {
|
if (this.preLoad) {
|
||||||
if (url in CONTEXTS) {
|
if (url in CONTEXTS) {
|
||||||
@ -126,7 +126,7 @@ class LdSignature {
|
|||||||
timeout: this.loderTimeout,
|
timeout: this.loderTimeout,
|
||||||
}, { throwErrorWhenResponseNotOk: false }).then(res => {
|
}, { throwErrorWhenResponseNotOk: false }).then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw `${res.status} ${res.statusText}`;
|
throw new Error(`${res.status} ${res.statusText}`);
|
||||||
} else {
|
} else {
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import { PollService } from '@/core/PollService.js';
|
|||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
@ -32,7 +33,6 @@ import { ApQuestionService } from './ApQuestionService.js';
|
|||||||
import { ApImageService } from './ApImageService.js';
|
import { ApImageService } from './ApImageService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IPost } from '../type.js';
|
import type { IObject, IPost } from '../type.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApNoteService {
|
export class ApNoteService {
|
||||||
@ -230,7 +230,7 @@ export class ApNoteService {
|
|||||||
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
||||||
if (!quote) {
|
if (!quote) {
|
||||||
if (results.some(x => x.status === 'temperror')) {
|
if (results.some(x => x.status === 'temperror')) {
|
||||||
throw 'quote resolve failed';
|
throw new Error('quote resolve failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,7 +311,7 @@ export class ApNoteService {
|
|||||||
|
|
||||||
// ブロックしてたら中断
|
// ブロックしてたら中断
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw { statusCode: 451 };
|
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
|
@ -32,6 +32,8 @@ import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags } from './tag.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
@ -42,8 +44,6 @@ import type { ApLoggerService } from '../ApLoggerService.js';
|
|||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
import type { ApImageService } from './ApImageService.js';
|
import type { ApImageService } from './ApImageService.js';
|
||||||
import type { IActor, IObject } from '../type.js';
|
import type { IActor, IObject } from '../type.js';
|
||||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
|
||||||
|
|
||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
const summaryLength = 2048;
|
const summaryLength = 2048;
|
||||||
@ -306,7 +306,6 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
tags,
|
tags,
|
||||||
isBot,
|
isBot,
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
showTimelineReplies: false,
|
|
||||||
})) as RemoteUser;
|
})) as RemoteUser;
|
||||||
|
|
||||||
await transactionalEntityManager.save(new UserProfile({
|
await transactionalEntityManager.save(new UserProfile({
|
||||||
@ -696,7 +695,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
|
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
|
||||||
return 'skip: dst.alsoKnownAs is empty';
|
return 'skip: dst.alsoKnownAs is empty';
|
||||||
}
|
}
|
||||||
if (!dst.alsoKnownAs?.includes(src.uri)) {
|
if (!dst.alsoKnownAs.includes(src.uri)) {
|
||||||
return 'skip: alsoKnownAs does not include from.uri';
|
return 'skip: alsoKnownAs does not include from.uri';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,8 @@ export class ChartManagementService implements OnApplicationShutdown {
|
|||||||
}, 1000 * 60 * 20);
|
}, 1000 * 60 * 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApplicationShutdown(signal: string): Promise<void> {
|
@bindThis
|
||||||
|
public async dispose(): Promise<void> {
|
||||||
clearInterval(this.saveIntervalId);
|
clearInterval(this.saveIntervalId);
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@ -68,4 +69,9 @@ export class ChartManagementService implements OnApplicationShutdown {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
async onApplicationShutdown(signal: string): Promise<void> {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ export class EmojiEntityService {
|
|||||||
category: emoji.category,
|
category: emoji.category,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
|
isSensitive: emoji.isSensitive ? true : undefined,
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +53,9 @@ export class EmojiEntityService {
|
|||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
license: emoji.license,
|
license: emoji.license,
|
||||||
|
isSensitive: emoji.isSensitive,
|
||||||
|
localOnly: emoji.localOnly,
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,7 +466,6 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
||||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
emailNotificationTypes: profile!.emailNotificationTypes,
|
||||||
showTimelineReplies: user.showTimelineReplies ?? falsy,
|
|
||||||
achievements: profile!.achievements,
|
achievements: profile!.achievements,
|
||||||
loggedInDays: profile!.loggedInDates.length,
|
loggedInDays: profile!.loggedInDates.length,
|
||||||
policies: this.roleService.getUserPolicies(user.id),
|
policies: this.roleService.getUserPolicies(user.id),
|
||||||
|
@ -35,6 +35,7 @@ export class UserListEntityService {
|
|||||||
createdAt: userList.createdAt.toISOString(),
|
createdAt: userList.createdAt.toISOString(),
|
||||||
name: userList.name,
|
name: userList.name,
|
||||||
userIds: users.map(x => x.userId),
|
userIds: users.map(x => x.userId),
|
||||||
|
isPublic: userList.isPublic,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,12 @@ export class JanitorService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public dispose(): void {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import Xev from 'xev';
|
import Xev from 'xev';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
@ -13,6 +17,9 @@ export class QueueStatsService implements OnApplicationShutdown {
|
|||||||
private intervalId: NodeJS.Timer;
|
private intervalId: NodeJS.Timer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@ -31,11 +38,14 @@ export class QueueStatsService implements OnApplicationShutdown {
|
|||||||
let activeDeliverJobs = 0;
|
let activeDeliverJobs = 0;
|
||||||
let activeInboxJobs = 0;
|
let activeInboxJobs = 0;
|
||||||
|
|
||||||
this.queueService.deliverQueue.on('global:active', () => {
|
const deliverQueueEvents = new Bull.QueueEvents(QUEUE.DELIVER, baseQueueOptions(this.config, QUEUE.DELIVER));
|
||||||
|
const inboxQueueEvents = new Bull.QueueEvents(QUEUE.INBOX, baseQueueOptions(this.config, QUEUE.INBOX));
|
||||||
|
|
||||||
|
deliverQueueEvents.on('active', () => {
|
||||||
activeDeliverJobs++;
|
activeDeliverJobs++;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queueService.inboxQueue.on('global:active', () => {
|
inboxQueueEvents.on('active', () => {
|
||||||
activeInboxJobs++;
|
activeInboxJobs++;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +83,12 @@ export class QueueStatsService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public dispose(): void {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,9 +63,14 @@ export class ServerStatsService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public dispose(): void {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU STAT
|
// CPU STAT
|
||||||
|
@ -25,6 +25,7 @@ export const DI = {
|
|||||||
userSecurityKeysRepository: Symbol('userSecurityKeysRepository'),
|
userSecurityKeysRepository: Symbol('userSecurityKeysRepository'),
|
||||||
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
||||||
userListsRepository: Symbol('userListsRepository'),
|
userListsRepository: Symbol('userListsRepository'),
|
||||||
|
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
|
||||||
userListJoiningsRepository: Symbol('userListJoiningsRepository'),
|
userListJoiningsRepository: Symbol('userListJoiningsRepository'),
|
||||||
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
||||||
userIpsRepository: Symbol('userIpsRepository'),
|
userIpsRepository: Symbol('userIpsRepository'),
|
||||||
|
@ -21,7 +21,7 @@ function getNoise(): string {
|
|||||||
|
|
||||||
export function genAid(date: Date): string {
|
export function genAid(date: Date): string {
|
||||||
const t = date.getTime();
|
const t = date.getTime();
|
||||||
if (isNaN(t)) throw 'Failed to create AID: Invalid Date';
|
if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date');
|
||||||
counter++;
|
counter++;
|
||||||
return getTime(t) + getNoise();
|
return getTime(t) + getNoise();
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@ const dateTimeIntervals = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function dateUTC(time: number[]): Date {
|
export function dateUTC(time: number[]): Date {
|
||||||
const d = time.length === 2 ? Date.UTC(time[0], time[1])
|
const d =
|
||||||
|
time.length === 2 ? Date.UTC(time[0], time[1])
|
||||||
: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
|
: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
|
||||||
: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
|
: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
|
||||||
: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
|
: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
|
||||||
@ -13,7 +14,7 @@ export function dateUTC(time: number[]): Date {
|
|||||||
: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
|
: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!d) throw 'wrong number of arguments';
|
if (!d) throw new Error('wrong number of arguments');
|
||||||
|
|
||||||
return new Date(d);
|
return new Date(d);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo } from './index.js';
|
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite } 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';
|
||||||
|
|
||||||
@ -112,6 +112,12 @@ const $userListsRepository: Provider = {
|
|||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $userListFavoritesRepository: Provider = {
|
||||||
|
provide: DI.userListFavoritesRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(UserListFavorite),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $userListJoiningsRepository: Provider = {
|
const $userListJoiningsRepository: Provider = {
|
||||||
provide: DI.userListJoiningsRepository,
|
provide: DI.userListJoiningsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(UserListJoining),
|
useFactory: (db: DataSource) => db.getRepository(UserListJoining),
|
||||||
@ -416,6 +422,7 @@ const $userMemosRepository: Provider = {
|
|||||||
$userSecurityKeysRepository,
|
$userSecurityKeysRepository,
|
||||||
$userPublickeysRepository,
|
$userPublickeysRepository,
|
||||||
$userListsRepository,
|
$userListsRepository,
|
||||||
|
$userListFavoritesRepository,
|
||||||
$userListJoiningsRepository,
|
$userListJoiningsRepository,
|
||||||
$userNotePiningsRepository,
|
$userNotePiningsRepository,
|
||||||
$userIpsRepository,
|
$userIpsRepository,
|
||||||
@ -483,6 +490,7 @@ const $userMemosRepository: Provider = {
|
|||||||
$userSecurityKeysRepository,
|
$userSecurityKeysRepository,
|
||||||
$userPublickeysRepository,
|
$userPublickeysRepository,
|
||||||
$userListsRepository,
|
$userListsRepository,
|
||||||
|
$userListFavoritesRepository,
|
||||||
$userListJoiningsRepository,
|
$userListJoiningsRepository,
|
||||||
$userNotePiningsRepository,
|
$userNotePiningsRepository,
|
||||||
$userIpsRepository,
|
$userIpsRepository,
|
||||||
|
@ -60,4 +60,20 @@ export class Emoji {
|
|||||||
length: 1024, nullable: true,
|
length: 1024, nullable: true,
|
||||||
})
|
})
|
||||||
public license: string | null;
|
public license: string | null;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public localOnly: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isSensitive: boolean;
|
||||||
|
|
||||||
|
// TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
|
||||||
|
@Column('varchar', {
|
||||||
|
array: true, length: 128, default: '{}',
|
||||||
|
})
|
||||||
|
public roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ export class Note {
|
|||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 64, nullable: true,
|
length: 64, nullable: true,
|
||||||
})
|
})
|
||||||
public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | null;
|
public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
|
||||||
|
|
||||||
@Column('smallint', {
|
@Column('smallint', {
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@ -232,12 +232,6 @@ export class User {
|
|||||||
})
|
})
|
||||||
public followersUri: string | null;
|
public followersUri: string | null;
|
||||||
|
|
||||||
@Column('boolean', {
|
|
||||||
default: false,
|
|
||||||
comment: 'Whether to show users replying to other users in the timeline.',
|
|
||||||
})
|
|
||||||
public showTimelineReplies: boolean;
|
|
||||||
|
|
||||||
@Index({ unique: true })
|
@Index({ unique: true })
|
||||||
@Column('char', {
|
@Column('char', {
|
||||||
length: 16, nullable: true, unique: true,
|
length: 16, nullable: true, unique: true,
|
||||||
|
@ -19,6 +19,12 @@ export class UserList {
|
|||||||
})
|
})
|
||||||
public userId: User['id'];
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isPublic: boolean;
|
||||||
|
|
||||||
@ManyToOne(type => User, {
|
@ManyToOne(type => User, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
|
33
packages/backend/src/models/entities/UserListFavorite.ts
Normal file
33
packages/backend/src/models/entities/UserListFavorite.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
|
import { id } from '../id.js';
|
||||||
|
import { User } from './User.js';
|
||||||
|
import { UserList } from './UserList.js';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['userId', 'userListId'], { unique: true })
|
||||||
|
export class UserListFavorite {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone')
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column(id())
|
||||||
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: User | null;
|
||||||
|
|
||||||
|
@Column(id())
|
||||||
|
public userListId: UserList['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => UserList, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public userList: UserList | null;
|
||||||
|
}
|
@ -49,6 +49,7 @@ import { User } from '@/models/entities/User.js';
|
|||||||
import { UserIp } from '@/models/entities/UserIp.js';
|
import { UserIp } from '@/models/entities/UserIp.js';
|
||||||
import { UserKeypair } from '@/models/entities/UserKeypair.js';
|
import { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||||
import { UserList } from '@/models/entities/UserList.js';
|
import { UserList } from '@/models/entities/UserList.js';
|
||||||
|
import { UserListFavorite } from './entities/UserListFavorite.js';
|
||||||
import { UserListJoining } from '@/models/entities/UserListJoining.js';
|
import { UserListJoining } from '@/models/entities/UserListJoining.js';
|
||||||
import { UserNotePining } from '@/models/entities/UserNotePining.js';
|
import { UserNotePining } from '@/models/entities/UserNotePining.js';
|
||||||
import { UserPending } from '@/models/entities/UserPending.js';
|
import { UserPending } from '@/models/entities/UserPending.js';
|
||||||
@ -117,6 +118,7 @@ export {
|
|||||||
UserIp,
|
UserIp,
|
||||||
UserKeypair,
|
UserKeypair,
|
||||||
UserList,
|
UserList,
|
||||||
|
UserListFavorite,
|
||||||
UserListJoining,
|
UserListJoining,
|
||||||
UserNotePining,
|
UserNotePining,
|
||||||
UserPending,
|
UserPending,
|
||||||
@ -184,6 +186,7 @@ export type UsersRepository = Repository<User>;
|
|||||||
export type UserIpsRepository = Repository<UserIp>;
|
export type UserIpsRepository = Repository<UserIp>;
|
||||||
export type UserKeypairsRepository = Repository<UserKeypair>;
|
export type UserKeypairsRepository = Repository<UserKeypair>;
|
||||||
export type UserListsRepository = Repository<UserList>;
|
export type UserListsRepository = Repository<UserList>;
|
||||||
|
export type UserListFavoritesRepository = Repository<UserListFavorite>;
|
||||||
export type UserListJoiningsRepository = Repository<UserListJoining>;
|
export type UserListJoiningsRepository = Repository<UserListJoining>;
|
||||||
export type UserNotePiningsRepository = Repository<UserNotePining>;
|
export type UserNotePiningsRepository = Repository<UserNotePining>;
|
||||||
export type UserPendingsRepository = Repository<UserPending>;
|
export type UserPendingsRepository = Repository<UserPending>;
|
||||||
|
@ -22,6 +22,19 @@ export const packedEmojiSimpleSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -63,5 +76,22 @@ export const packedEmojiDetailedSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
localOnly: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -25,5 +25,10 @@ export const packedUserListSchema = {
|
|||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
isPublic: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false,
|
||||||
|
optional: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -57,6 +57,7 @@ import { User } from '@/models/entities/User.js';
|
|||||||
import { UserIp } from '@/models/entities/UserIp.js';
|
import { UserIp } from '@/models/entities/UserIp.js';
|
||||||
import { UserKeypair } from '@/models/entities/UserKeypair.js';
|
import { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||||
import { UserList } from '@/models/entities/UserList.js';
|
import { UserList } from '@/models/entities/UserList.js';
|
||||||
|
import { UserListFavorite } from '@/models/entities/UserListFavorite.js';
|
||||||
import { UserListJoining } from '@/models/entities/UserListJoining.js';
|
import { UserListJoining } from '@/models/entities/UserListJoining.js';
|
||||||
import { UserNotePining } from '@/models/entities/UserNotePining.js';
|
import { UserNotePining } from '@/models/entities/UserNotePining.js';
|
||||||
import { UserPending } from '@/models/entities/UserPending.js';
|
import { UserPending } from '@/models/entities/UserPending.js';
|
||||||
@ -132,6 +133,7 @@ export const entities = [
|
|||||||
UserKeypair,
|
UserKeypair,
|
||||||
UserPublickey,
|
UserPublickey,
|
||||||
UserList,
|
UserList,
|
||||||
|
UserListFavorite,
|
||||||
UserListJoining,
|
UserListJoining,
|
||||||
UserNotePining,
|
UserNotePining,
|
||||||
UserSecurityKey,
|
UserSecurityKey,
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { getJobInfo } from './get-job-info.js';
|
|
||||||
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||||
@ -35,17 +34,51 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
|
|||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
|
import { QUEUE, baseQueueOptions } from './const.js';
|
||||||
|
|
||||||
|
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||||
|
function httpRelatedBackoff(attemptsMade: number) {
|
||||||
|
const baseDelay = 60 * 1000; // 1min
|
||||||
|
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
|
||||||
|
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
||||||
|
backoff = Math.min(backoff, maxBackoff);
|
||||||
|
backoff += Math.round(backoff * Math.random() * 0.2);
|
||||||
|
return backoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJobInfo(job: Bull.Job | undefined, increment = false): string {
|
||||||
|
if (job == null) return '-';
|
||||||
|
|
||||||
|
const age = Date.now() - job.timestamp;
|
||||||
|
|
||||||
|
const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
|
||||||
|
: age > 10000 ? `${Math.floor(age / 1000)}s`
|
||||||
|
: `${age}ms`;
|
||||||
|
|
||||||
|
// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
|
||||||
|
const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
|
||||||
|
const maxAttempts = job.opts ? job.opts.attempts : 0;
|
||||||
|
|
||||||
|
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueProcessorService {
|
export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private systemQueueWorker: Bull.Worker;
|
||||||
|
private dbQueueWorker: Bull.Worker;
|
||||||
|
private deliverQueueWorker: Bull.Worker;
|
||||||
|
private inboxQueueWorker: Bull.Worker;
|
||||||
|
private webhookDeliverQueueWorker: Bull.Worker;
|
||||||
|
private relationshipQueueWorker: Bull.Worker;
|
||||||
|
private objectStorageQueueWorker: Bull.Worker;
|
||||||
|
private endedPollNotificationQueueWorker: Bull.Worker;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
private queueService: QueueService,
|
|
||||||
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
|
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
|
||||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||||
private deliverProcessorService: DeliverProcessorService,
|
private deliverProcessorService: DeliverProcessorService,
|
||||||
@ -77,10 +110,7 @@ export class QueueProcessorService {
|
|||||||
private cleanProcessorService: CleanProcessorService,
|
private cleanProcessorService: CleanProcessorService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger;
|
this.logger = this.queueLoggerService.logger;
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public start() {
|
|
||||||
function renderError(e: Error): any {
|
function renderError(e: Error): any {
|
||||||
if (e) { // 何故かeがundefinedで来ることがある
|
if (e) { // 何故かeがundefinedで来ることがある
|
||||||
return {
|
return {
|
||||||
@ -97,146 +127,232 @@ export class QueueProcessorService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemLogger = this.logger.createSubLogger('system');
|
//#region system
|
||||||
const deliverLogger = this.logger.createSubLogger('deliver');
|
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||||
const webhookLogger = this.logger.createSubLogger('webhook');
|
switch (job.name) {
|
||||||
const inboxLogger = this.logger.createSubLogger('inbox');
|
case 'tickCharts': return this.tickChartsProcessorService.process();
|
||||||
const dbLogger = this.logger.createSubLogger('db');
|
case 'resyncCharts': return this.resyncChartsProcessorService.process();
|
||||||
const relationshipLogger = this.logger.createSubLogger('relationship');
|
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
||||||
const objectStorageLogger = this.logger.createSubLogger('objectStorage');
|
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
||||||
|
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
||||||
|
case 'clean': return this.cleanProcessorService.process();
|
||||||
|
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.SYSTEM),
|
||||||
|
autorun: false,
|
||||||
|
});
|
||||||
|
|
||||||
this.queueService.systemQueue
|
const systemLogger = this.logger.createSubLogger('system');
|
||||||
.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
|
|
||||||
|
this.systemQueueWorker
|
||||||
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
|
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
||||||
.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
.on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||||
.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
|
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
this.queueService.deliverQueue
|
//#region db
|
||||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
||||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
switch (job.name) {
|
||||||
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
||||||
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
||||||
.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
||||||
.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
||||||
|
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
||||||
|
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
||||||
|
case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
|
||||||
|
case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
|
||||||
|
case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
|
||||||
|
case 'importFollowing': return this.importFollowingProcessorService.process(job);
|
||||||
|
case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
|
||||||
|
case 'importMuting': return this.importMutingProcessorService.process(job);
|
||||||
|
case 'importBlocking': return this.importBlockingProcessorService.process(job);
|
||||||
|
case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
|
||||||
|
case 'importUserLists': return this.importUserListsProcessorService.process(job);
|
||||||
|
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
|
||||||
|
case 'importAntennas': return this.importAntennasProcessorService.process(job);
|
||||||
|
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
|
||||||
|
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.DB),
|
||||||
|
autorun: false,
|
||||||
|
});
|
||||||
|
|
||||||
this.queueService.inboxQueue
|
const dbLogger = this.logger.createSubLogger('db');
|
||||||
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
|
|
||||||
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
|
|
||||||
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
|
||||||
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
|
|
||||||
.on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
|
||||||
.on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
|
|
||||||
|
|
||||||
this.queueService.dbQueue
|
this.dbQueueWorker
|
||||||
.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
|
|
||||||
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
|
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
||||||
.on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
.on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||||
.on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`));
|
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
this.queueService.relationshipQueue
|
//#region deliver
|
||||||
.on('waiting', (jobId) => relationshipLogger.debug(`waiting id=${jobId}`))
|
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
|
||||||
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
|
...baseQueueOptions(this.config, QUEUE.DELIVER),
|
||||||
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
|
autorun: false,
|
||||||
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
concurrency: this.config.deliverJobConcurrency ?? 128,
|
||||||
.on('error', (job: any, err: Error) => relationshipLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
limiter: {
|
||||||
.on('stalled', (job) => relationshipLogger.warn(`stalled id=${job.id}`));
|
max: this.config.deliverJobPerSec ?? 128,
|
||||||
|
duration: 1000,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
backoffStrategy: httpRelatedBackoff,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.queueService.objectStorageQueue
|
const deliverLogger = this.logger.createSubLogger('deliver');
|
||||||
.on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`))
|
|
||||||
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
|
|
||||||
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
|
|
||||||
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
|
||||||
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
|
||||||
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
|
|
||||||
|
|
||||||
this.queueService.webhookDeliverQueue
|
this.deliverQueueWorker
|
||||||
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
|
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
|
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
|
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
|
||||||
|
.on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region inbox
|
||||||
|
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.INBOX),
|
||||||
|
autorun: false,
|
||||||
|
concurrency: this.config.inboxJobConcurrency ?? 16,
|
||||||
|
limiter: {
|
||||||
|
max: this.config.inboxJobPerSec ?? 16,
|
||||||
|
duration: 1000,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
backoffStrategy: httpRelatedBackoff,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inboxLogger = this.logger.createSubLogger('inbox');
|
||||||
|
|
||||||
|
this.inboxQueueWorker
|
||||||
|
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
|
||||||
|
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||||
|
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
|
||||||
|
.on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region webhook deliver
|
||||||
|
this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
|
||||||
|
autorun: false,
|
||||||
|
concurrency: 64,
|
||||||
|
limiter: {
|
||||||
|
max: 64,
|
||||||
|
duration: 1000,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
backoffStrategy: httpRelatedBackoff,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const webhookLogger = this.logger.createSubLogger('webhook');
|
||||||
|
|
||||||
|
this.webhookDeliverQueueWorker
|
||||||
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
|
||||||
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
.on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||||
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
this.queueService.systemQueue.add('tickCharts', {
|
//#region relationship
|
||||||
|
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
||||||
|
switch (job.name) {
|
||||||
|
case 'follow': return this.relationshipProcessorService.processFollow(job);
|
||||||
|
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
|
||||||
|
case 'block': return this.relationshipProcessorService.processBlock(job);
|
||||||
|
case 'unblock': return this.relationshipProcessorService.processUnblock(job);
|
||||||
|
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
repeat: { cron: '55 * * * *' },
|
...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
|
||||||
removeOnComplete: true,
|
autorun: false,
|
||||||
|
concurrency: this.config.relashionshipJobConcurrency ?? 16,
|
||||||
|
limiter: {
|
||||||
|
max: this.config.relashionshipJobPerSec ?? 64,
|
||||||
|
duration: 1000,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queueService.systemQueue.add('resyncCharts', {
|
const relationshipLogger = this.logger.createSubLogger('relationship');
|
||||||
|
|
||||||
|
this.relationshipQueueWorker
|
||||||
|
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
|
||||||
|
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
|
||||||
|
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
||||||
|
.on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region object storage
|
||||||
|
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
||||||
|
switch (job.name) {
|
||||||
|
case 'deleteFile': return this.deleteFileProcessorService.process(job);
|
||||||
|
case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
|
||||||
|
default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
repeat: { cron: '0 0 * * *' },
|
...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
|
||||||
removeOnComplete: true,
|
autorun: false,
|
||||||
|
concurrency: 16,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queueService.systemQueue.add('cleanCharts', {
|
const objectStorageLogger = this.logger.createSubLogger('objectStorage');
|
||||||
}, {
|
|
||||||
repeat: { cron: '0 0 * * *' },
|
this.objectStorageQueueWorker
|
||||||
removeOnComplete: true,
|
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
|
||||||
|
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
|
||||||
|
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
|
||||||
|
.on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) }))
|
||||||
|
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region ended poll notification
|
||||||
|
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||||
|
autorun: false,
|
||||||
});
|
});
|
||||||
|
//#endregion
|
||||||
this.queueService.systemQueue.add('aggregateRetention', {
|
|
||||||
}, {
|
|
||||||
repeat: { cron: '0 0 * * *' },
|
|
||||||
removeOnComplete: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queueService.systemQueue.add('clean', {
|
|
||||||
}, {
|
|
||||||
repeat: { cron: '0 0 * * *' },
|
|
||||||
removeOnComplete: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queueService.systemQueue.add('checkExpiredMutings', {
|
|
||||||
}, {
|
|
||||||
repeat: { cron: '*/5 * * * *' },
|
|
||||||
removeOnComplete: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job));
|
|
||||||
this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job));
|
|
||||||
this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done));
|
|
||||||
this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job));
|
|
||||||
|
|
||||||
this.queueService.dbQueue.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
|
|
||||||
this.queueService.dbQueue.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
|
|
||||||
this.queueService.dbQueue.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done));
|
|
||||||
this.queueService.dbQueue.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));
|
|
||||||
|
|
||||||
this.queueService.objectStorageQueue.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job));
|
|
||||||
this.queueService.objectStorageQueue.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done));
|
|
||||||
|
|
||||||
{
|
|
||||||
const maxJobs = this.config.relashionshipJobConcurrency ?? 16;
|
|
||||||
this.queueService.relationshipQueue.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
|
|
||||||
this.queueService.relationshipQueue.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
|
|
||||||
this.queueService.relationshipQueue.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
|
|
||||||
this.queueService.relationshipQueue.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queueService.systemQueue.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done));
|
@bindThis
|
||||||
this.queueService.systemQueue.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done));
|
public async start(): Promise<void> {
|
||||||
this.queueService.systemQueue.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done));
|
await Promise.all([
|
||||||
this.queueService.systemQueue.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done));
|
this.systemQueueWorker.run(),
|
||||||
this.queueService.systemQueue.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done));
|
this.dbQueueWorker.run(),
|
||||||
this.queueService.systemQueue.process('clean', (job, done) => this.cleanProcessorService.process(job, done));
|
this.deliverQueueWorker.run(),
|
||||||
|
this.inboxQueueWorker.run(),
|
||||||
|
this.webhookDeliverQueueWorker.run(),
|
||||||
|
this.relationshipQueueWorker.run(),
|
||||||
|
this.objectStorageQueueWorker.run(),
|
||||||
|
this.endedPollNotificationQueueWorker.run(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
this.systemQueueWorker.close(),
|
||||||
|
this.dbQueueWorker.close(),
|
||||||
|
this.deliverQueueWorker.close(),
|
||||||
|
this.inboxQueueWorker.close(),
|
||||||
|
this.webhookDeliverQueueWorker.close(),
|
||||||
|
this.relationshipQueueWorker.close(),
|
||||||
|
this.objectStorageQueueWorker.close(),
|
||||||
|
this.endedPollNotificationQueueWorker.close(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
|
||||||
|
await this.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
packages/backend/src/queue/const.ts
Normal file
26
packages/backend/src/queue/const.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Config } from '@/config.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
|
export const QUEUE = {
|
||||||
|
DELIVER: 'deliver',
|
||||||
|
INBOX: 'inbox',
|
||||||
|
SYSTEM: 'system',
|
||||||
|
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
|
||||||
|
DB: 'db',
|
||||||
|
RELATIONSHIP: 'relationship',
|
||||||
|
OBJECT_STORAGE: 'objectStorage',
|
||||||
|
WEBHOOK_DELIVER: 'webhookDeliver',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
|
||||||
|
return {
|
||||||
|
connection: {
|
||||||
|
port: config.redisForJobQueue.port,
|
||||||
|
host: config.redisForJobQueue.host,
|
||||||
|
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
|
||||||
|
password: config.redisForJobQueue.pass,
|
||||||
|
db: config.redisForJobQueue.db ?? 0,
|
||||||
|
},
|
||||||
|
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`,
|
||||||
|
};
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
import Bull from 'bull';
|
|
||||||
|
|
||||||
export function getJobInfo(job: Bull.Job, increment = false) {
|
|
||||||
const age = Date.now() - job.timestamp;
|
|
||||||
|
|
||||||
const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
|
|
||||||
: age > 10000 ? `${Math.floor(age / 1000)}s`
|
|
||||||
: `${age}ms`;
|
|
||||||
|
|
||||||
// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
|
|
||||||
const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
|
|
||||||
const maxAttempts = job.opts ? job.opts.attempts : 0;
|
|
||||||
|
|
||||||
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ import { deepClone } from '@/misc/clone.js';
|
|||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AggregateRetentionProcessorService {
|
export class AggregateRetentionProcessorService {
|
||||||
@ -32,7 +32,7 @@ export class AggregateRetentionProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
public async process(): Promise<void> {
|
||||||
this.logger.info('Aggregating retention...');
|
this.logger.info('Aggregating retention...');
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -62,7 +62,6 @@ export class AggregateRetentionProcessorService {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isDuplicateKeyValueError(err)) {
|
if (isDuplicateKeyValueError(err)) {
|
||||||
this.logger.succ('Skip because it has already been processed by another worker.');
|
this.logger.succ('Skip because it has already been processed by another worker.');
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
@ -88,6 +87,5 @@ export class AggregateRetentionProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('Retention aggregated.');
|
this.logger.succ('Retention aggregated.');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import type Logger from '@/logger.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CheckExpiredMutingsProcessorService {
|
export class CheckExpiredMutingsProcessorService {
|
||||||
@ -27,7 +27,7 @@ export class CheckExpiredMutingsProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
public async process(): Promise<void> {
|
||||||
this.logger.info('Checking expired mutings...');
|
this.logger.info('Checking expired mutings...');
|
||||||
|
|
||||||
const expired = await this.mutingsRepository.createQueryBuilder('muting')
|
const expired = await this.mutingsRepository.createQueryBuilder('muting')
|
||||||
@ -41,6 +41,5 @@ export class CheckExpiredMutingsProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('All expired mutings checked.');
|
this.logger.succ('All expired mutings checked.');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
|
|||||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CleanChartsProcessorService {
|
export class CleanChartsProcessorService {
|
||||||
@ -45,7 +45,7 @@ export class CleanChartsProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
public async process(): Promise<void> {
|
||||||
this.logger.info('Clean charts...');
|
this.logger.info('Clean charts...');
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -64,6 +64,5 @@ export class CleanChartsProcessorService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
this.logger.succ('All charts successfully cleaned.');
|
this.logger.succ('All charts successfully cleaned.');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ 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 { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CleanProcessorService {
|
export class CleanProcessorService {
|
||||||
@ -36,7 +36,7 @@ export class CleanProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
public async process(): Promise<void> {
|
||||||
this.logger.info('Cleaning...');
|
this.logger.info('Cleaning...');
|
||||||
|
|
||||||
this.userIpsRepository.delete({
|
this.userIpsRepository.delete({
|
||||||
@ -72,6 +72,5 @@ export class CleanProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('Cleaned.');
|
this.logger.succ('Cleaned.');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@ import type { DriveFilesRepository } from '@/models/index.js';
|
|||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CleanRemoteFilesProcessorService {
|
export class CleanRemoteFilesProcessorService {
|
||||||
@ -27,7 +27,7 @@ export class CleanRemoteFilesProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<Record<string, unknown>>): Promise<void> {
|
||||||
this.logger.info('Deleting cached remote files...');
|
this.logger.info('Deleting cached remote files...');
|
||||||
|
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
@ -47,7 +47,7 @@ export class CleanRemoteFilesProcessorService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
job.progress(100);
|
job.updateProgress(100);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,10 +62,9 @@ export class CleanRemoteFilesProcessorService {
|
|||||||
isLink: false,
|
isLink: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
job.progress(deletedCount / total);
|
job.updateProgress(deletedCount / total);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('All cached remote files has been deleted.');
|
this.logger.succ('All cached remote files has been deleted.');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,10 @@ import { DriveService } from '@/core/DriveService.js';
|
|||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { EmailService } from '@/core/EmailService.js';
|
import { EmailService } from '@/core/EmailService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbUserDeleteJobData } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbUserDeleteJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeleteAccountProcessorService {
|
export class DeleteAccountProcessorService {
|
||||||
|
@ -5,10 +5,10 @@ import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
|
|||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeleteDriveFilesProcessorService {
|
export class DeleteDriveFilesProcessorService {
|
||||||
@ -31,12 +31,11 @@ export class DeleteDriveFilesProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
this.logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +55,7 @@ export class DeleteDriveFilesProcessorService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
job.progress(100);
|
job.updateProgress(100);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,10 +70,9 @@ export class DeleteDriveFilesProcessorService {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
job.progress(deletedCount / total);
|
job.updateProgress(deletedCount / total);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
|
this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { ObjectStorageFileJobData } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { ObjectStorageFileJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeleteFileProcessorService {
|
export class DeleteFileProcessorService {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js';
|
import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
@ -16,7 +17,6 @@ import { StatusError } from '@/misc/status-error.js';
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DeliverJobData } from '../types.js';
|
import type { DeliverJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -121,15 +121,13 @@ export class DeliverProcessorService {
|
|||||||
isSuspended: true,
|
isSuspended: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return `${host} is gone`;
|
throw new Bull.UnrecoverableError(`${host} is gone`);
|
||||||
}
|
}
|
||||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||||
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
|
||||||
return `${res.statusCode} ${res.statusMessage}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5xx etc.
|
// 5xx etc.
|
||||||
throw `${res.statusCode} ${res.statusMessage}`;
|
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
||||||
} else {
|
} else {
|
||||||
// DNS error, socket error, timeout ...
|
// DNS error, socket error, timeout ...
|
||||||
throw res;
|
throw res;
|
||||||
|
@ -6,7 +6,7 @@ import type Logger from '@/logger.js';
|
|||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
import type { EndedPollNotificationJobData } from '../types.js';
|
import type { EndedPollNotificationJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -30,10 +30,9 @@ export class EndedPollNotificationProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<EndedPollNotificationJobData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<EndedPollNotificationJobData>): Promise<void> {
|
||||||
const note = await this.notesRepository.findOneBy({ id: job.data.noteId });
|
const note = await this.notesRepository.findOneBy({ id: job.data.noteId });
|
||||||
if (note == null || !note.hasPoll) {
|
if (note == null || !note.hasPoll) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +50,5 @@ export class EndedPollNotificationProcessorService {
|
|||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { createTemp } from '@/misc/create-temp.js';
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type { DBExportAntennasData } from '../types.js';
|
import type { DBExportAntennasData } from '../types.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportAntennasProcessorService {
|
export class ExportAntennasProcessorService {
|
||||||
@ -39,10 +39,9 @@ export class ExportAntennasProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DBExportAntennasData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DBExportAntennasData>): Promise<void> {
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
@ -96,7 +95,6 @@ export class ExportAntennasProcessorService {
|
|||||||
this.logger.succ('Exported to: ' + driveFile.id);
|
this.logger.succ('Exported to: ' + driveFile.id);
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
|
|||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportBlockingProcessorService {
|
export class ExportBlockingProcessorService {
|
||||||
@ -36,12 +36,11 @@ export class ExportBlockingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting blocking of ${job.data.user.id} ...`);
|
this.logger.info(`Exporting blocking of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +68,7 @@ export class ExportBlockingProcessorService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (blockings.length === 0) {
|
if (blockings.length === 0) {
|
||||||
job.progress(100);
|
job.updateProgress(100);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +98,7 @@ export class ExportBlockingProcessorService {
|
|||||||
blockerId: user.id,
|
blockerId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
job.progress(exportedCount / total);
|
job.updateProgress(exportedCount / total);
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.end();
|
stream.end();
|
||||||
@ -112,7 +111,5 @@ export class ExportBlockingProcessorService {
|
|||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
|||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportCustomEmojisProcessorService {
|
export class ExportCustomEmojisProcessorService {
|
||||||
@ -37,12 +37,11 @@ export class ExportCustomEmojisProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job, done: () => void): Promise<void> {
|
public async process(job: Bull.Job): Promise<void> {
|
||||||
this.logger.info('Exporting custom emojis ...');
|
this.logger.info('Exporting custom emojis ...');
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +116,7 @@ export class ExportCustomEmojisProcessorService {
|
|||||||
metaStream.end();
|
metaStream.end();
|
||||||
|
|
||||||
// Create archive
|
// Create archive
|
||||||
|
await new Promise<void>(async (resolve) => {
|
||||||
const [archivePath, archiveCleanup] = await createTemp();
|
const [archivePath, archiveCleanup] = await createTemp();
|
||||||
const archiveStream = fs.createWriteStream(archivePath);
|
const archiveStream = fs.createWriteStream(archivePath);
|
||||||
const archive = archiver('zip', {
|
const archive = archiver('zip', {
|
||||||
@ -131,10 +131,11 @@ export class ExportCustomEmojisProcessorService {
|
|||||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
cleanup();
|
cleanup();
|
||||||
archiveCleanup();
|
archiveCleanup();
|
||||||
done();
|
resolve();
|
||||||
});
|
});
|
||||||
archive.pipe(archiveStream);
|
archive.pipe(archiveStream);
|
||||||
archive.directory(path, false);
|
archive.directory(path, false);
|
||||||
archive.finalize();
|
archive.finalize();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js';
|
|||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -42,12 +42,11 @@ export class ExportFavoritesProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting favorites of ${job.data.user.id} ...`);
|
this.logger.info(`Exporting favorites of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +90,7 @@ export class ExportFavoritesProcessorService {
|
|||||||
}) as (NoteFavorite & { note: Note & { user: User } })[];
|
}) as (NoteFavorite & { note: Note & { user: User } })[];
|
||||||
|
|
||||||
if (favorites.length === 0) {
|
if (favorites.length === 0) {
|
||||||
job.progress(100);
|
job.updateProgress(100);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +111,7 @@ export class ExportFavoritesProcessorService {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
job.progress(exportedFavoritesCount / total);
|
job.updateProgress(exportedFavoritesCount / total);
|
||||||
}
|
}
|
||||||
|
|
||||||
await write(']');
|
await write(']');
|
||||||
@ -127,8 +126,6 @@ export class ExportFavoritesProcessorService {
|
|||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@ import { DriveService } from '@/core/DriveService.js';
|
|||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import type { Following } from '@/models/entities/Following.js';
|
import type { Following } from '@/models/entities/Following.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbExportFollowingData } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbExportFollowingData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportFollowingProcessorService {
|
export class ExportFollowingProcessorService {
|
||||||
@ -40,12 +40,11 @@ export class ExportFollowingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbExportFollowingData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbExportFollowingData>): Promise<void> {
|
||||||
this.logger.info(`Exporting following of ${job.data.user.id} ...`);
|
this.logger.info(`Exporting following of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +115,5 @@ export class ExportFollowingProcessorService {
|
|||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
|
|||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportMutingProcessorService {
|
export class ExportMutingProcessorService {
|
||||||
@ -39,12 +39,11 @@ export class ExportMutingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting muting of ${job.data.user.id} ...`);
|
this.logger.info(`Exporting muting of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +72,7 @@ export class ExportMutingProcessorService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (mutes.length === 0) {
|
if (mutes.length === 0) {
|
||||||
job.progress(100);
|
job.updateProgress(100);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +102,7 @@ export class ExportMutingProcessorService {
|
|||||||
muterId: user.id,
|
muterId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
job.progress(exportedCount / total);
|
job.updateProgress(exportedCount / total);
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.end();
|
stream.end();
|
||||||
@ -116,7 +115,5 @@ export class ExportMutingProcessorService {
|
|||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js';
|
|||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -39,12 +39,11 @@ export class ExportNotesProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting notes of ${job.data.user.id} ...`);
|
this.logger.info(`Exporting notes of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +86,7 @@ export class ExportNotesProcessorService {
|
|||||||
}) as Note[];
|
}) as Note[];
|
||||||
|
|
||||||
if (notes.length === 0) {
|
if (notes.length === 0) {
|
||||||
job.progress(100);
|
job.updateProgress(100);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +107,7 @@ export class ExportNotesProcessorService {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
job.progress(exportedNotesCount / total);
|
job.updateProgress(exportedNotesCount / total);
|
||||||
}
|
}
|
||||||
|
|
||||||
await write(']');
|
await write(']');
|
||||||
@ -123,8 +122,6 @@ export class ExportNotesProcessorService {
|
|||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@ import type Logger from '@/logger.js';
|
|||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportUserListsProcessorService {
|
export class ExportUserListsProcessorService {
|
||||||
@ -39,12 +39,11 @@ export class ExportUserListsProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
this.logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +91,5 @@ export class ExportUserListsProcessorService {
|
|||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import { DBAntennaImportJobData } from '../types.js';
|
import { DBAntennaImportJobData } from '../types.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
const validate = new Ajv().compile({
|
const validate = new Ajv().compile({
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -59,7 +59,7 @@ export class ImportAntennasProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DBAntennaImportJobData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DBAntennaImportJobData>): Promise<void> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
try {
|
try {
|
||||||
for (const antenna of job.data.antenna) {
|
for (const antenna of job.data.antenna) {
|
||||||
@ -89,8 +89,6 @@ export class ImportAntennasProcessorService {
|
|||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
} finally {
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js';
|
|||||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportBlockingProcessorService {
|
export class ImportBlockingProcessorService {
|
||||||
@ -34,12 +34,11 @@ export class ImportBlockingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Importing blocking of ${job.data.user.id} ...`);
|
this.logger.info(`Importing blocking of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +46,6 @@ export class ImportBlockingProcessorService {
|
|||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +54,6 @@ export class ImportBlockingProcessorService {
|
|||||||
this.queueService.createImportBlockingToDbJob({ id: user.id }, targets);
|
this.queueService.createImportBlockingToDbJob({ id: user.id }, targets);
|
||||||
|
|
||||||
this.logger.succ('Import jobs created');
|
this.logger.succ('Import jobs created');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@ -85,7 +82,7 @@ export class ImportBlockingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
throw `Unable to resolve user: @${username}@${host}`;
|
throw new Error(`Unable to resolve user: @${username}@${host}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip myself
|
// skip myself
|
||||||
|
@ -12,7 +12,7 @@ import { DriveService } from '@/core/DriveService.js';
|
|||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbUserImportJobData } from '../types.js';
|
import type { DbUserImportJobData } from '../types.js';
|
||||||
|
|
||||||
// TODO: 名前衝突時の動作を選べるようにする
|
// TODO: 名前衝突時の動作を選べるようにする
|
||||||
@ -45,14 +45,13 @@ export class ImportCustomEmojisProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info('Importing custom emojis ...');
|
this.logger.info('Importing custom emojis ...');
|
||||||
|
|
||||||
const file = await this.driveFilesRepository.findOneBy({
|
const file = await this.driveFilesRepository.findOneBy({
|
||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,13 +106,15 @@ export class ImportCustomEmojisProcessorService {
|
|||||||
aliases: emojiInfo.aliases,
|
aliases: emojiInfo.aliases,
|
||||||
driveFile,
|
driveFile,
|
||||||
license: emojiInfo.license,
|
license: emojiInfo.license,
|
||||||
|
isSensitive: emojiInfo.isSensitive,
|
||||||
|
localOnly: emojiInfo.localOnly,
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
this.logger.succ('Imported');
|
this.logger.succ('Imported');
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
unzipStream.pipe(extractor);
|
unzipStream.pipe(extractor);
|
||||||
this.logger.succ(`Unzipping to ${outputPath}`);
|
this.logger.succ(`Unzipping to ${outputPath}`);
|
||||||
|
@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js';
|
|||||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportFollowingProcessorService {
|
export class ImportFollowingProcessorService {
|
||||||
@ -34,12 +34,11 @@ export class ImportFollowingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Importing following of ${job.data.user.id} ...`);
|
this.logger.info(`Importing following of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +46,6 @@ export class ImportFollowingProcessorService {
|
|||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +54,6 @@ export class ImportFollowingProcessorService {
|
|||||||
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets);
|
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets);
|
||||||
|
|
||||||
this.logger.succ('Import jobs created');
|
this.logger.succ('Import jobs created');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@ -85,7 +82,7 @@ export class ImportFollowingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
throw `Unable to resolve user: @${username}@${host}`;
|
throw new Error(`Unable to resolve user: @${username}@${host}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip myself
|
// skip myself
|
||||||
|
@ -9,10 +9,10 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
|||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { DbUserImportJobData } from '../types.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbUserImportJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportMutingProcessorService {
|
export class ImportMutingProcessorService {
|
||||||
@ -38,12 +38,11 @@ export class ImportMutingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Importing muting of ${job.data.user.id} ...`);
|
this.logger.info(`Importing muting of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +50,6 @@ export class ImportMutingProcessorService {
|
|||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +81,7 @@ export class ImportMutingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
throw `cannot resolve user: @${username}@${host}`;
|
throw new Error(`cannot resolve user: @${username}@${host}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip myself
|
// skip myself
|
||||||
@ -98,6 +96,5 @@ export class ImportMutingProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('Imported');
|
this.logger.succ('Imported');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { IdService } from '@/core/IdService.js';
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbUserImportJobData } from '../types.js';
|
import type { DbUserImportJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -46,12 +46,11 @@ export class ImportUserListsProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>, done: () => void): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Importing user lists of ${job.data.user.id} ...`);
|
this.logger.info(`Importing user lists of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +58,6 @@ export class ImportUserListsProcessorService {
|
|||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
done();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +107,5 @@ export class ImportUserListsProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('Imported');
|
this.logger.succ('Imported');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import httpSignature from '@peertube/http-signature';
|
import httpSignature from '@peertube/http-signature';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { InstancesRepository, DriveFilesRepository } from '@/models/index.js';
|
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
@ -23,10 +23,8 @@ import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
|
|||||||
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
|
||||||
import type { InboxJobData } from '../types.js';
|
import type { InboxJobData } from '../types.js';
|
||||||
|
|
||||||
// ユーザーのinboxにアクティビティが届いた時の処理
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InboxProcessorService {
|
export class InboxProcessorService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -35,12 +33,6 @@ export class InboxProcessorService {
|
|||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
|
||||||
private instancesRepository: InstancesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private apInboxService: ApInboxService,
|
private apInboxService: ApInboxService,
|
||||||
@ -96,21 +88,21 @@ export class InboxProcessorService {
|
|||||||
// 対象が4xxならスキップ
|
// 対象が4xxならスキップ
|
||||||
if (err instanceof StatusError) {
|
if (err instanceof StatusError) {
|
||||||
if (err.isClientError) {
|
if (err.isClientError) {
|
||||||
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`;
|
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
||||||
}
|
}
|
||||||
throw `Error in actor ${activity.actor} - ${err.statusCode ?? err}`;
|
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode ?? err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// それでもわからなければ終了
|
// それでもわからなければ終了
|
||||||
if (authUser == null) {
|
if (authUser == null) {
|
||||||
return 'skip: failed to resolve user';
|
throw new Bull.UnrecoverableError('skip: failed to resolve user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// publicKey がなくても終了
|
// publicKey がなくても終了
|
||||||
if (authUser.key == null) {
|
if (authUser.key == null) {
|
||||||
return 'skip: failed to resolve user publicKey';
|
throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey');
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP-Signatureの検証
|
// HTTP-Signatureの検証
|
||||||
@ -121,7 +113,7 @@ export class InboxProcessorService {
|
|||||||
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
||||||
if (activity.signature) {
|
if (activity.signature) {
|
||||||
if (activity.signature.type !== 'RsaSignature2017') {
|
if (activity.signature.type !== 'RsaSignature2017') {
|
||||||
return `skip: unsupported LD-signature type ${activity.signature.type}`;
|
throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// activity.signature.creator: https://example.oom/users/user#main-key
|
// activity.signature.creator: https://example.oom/users/user#main-key
|
||||||
@ -134,32 +126,32 @@ export class InboxProcessorService {
|
|||||||
// keyIdからLD-Signatureのユーザーを取得
|
// keyIdからLD-Signatureのユーザーを取得
|
||||||
authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator);
|
authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator);
|
||||||
if (authUser == null) {
|
if (authUser == null) {
|
||||||
return 'skip: LD-Signatureのユーザーが取得できませんでした';
|
throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authUser.key == null) {
|
if (authUser.key == null) {
|
||||||
return 'skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした';
|
throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
|
||||||
}
|
}
|
||||||
|
|
||||||
// LD-Signature検証
|
// LD-Signature検証
|
||||||
const ldSignature = this.ldSignatureService.use();
|
const ldSignature = this.ldSignatureService.use();
|
||||||
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
return 'skip: LD-Signatureの検証に失敗しました';
|
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
||||||
}
|
}
|
||||||
|
|
||||||
// もう一度actorチェック
|
// もう一度actorチェック
|
||||||
if (authUser.user.uri !== activity.actor) {
|
if (authUser.user.uri !== activity.actor) {
|
||||||
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
|
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ブロックしてたら中断
|
// ブロックしてたら中断
|
||||||
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
||||||
return `Blocked request: ${ldHost}`;
|
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
|
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +160,7 @@ export class InboxProcessorService {
|
|||||||
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
|
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
|
||||||
const activityIdHost = this.utilityService.extractDbHost(activity.id);
|
const activityIdHost = this.utilityService.extractDbHost(activity.id);
|
||||||
if (signerHost !== activityIdHost) {
|
if (signerHost !== activityIdHost) {
|
||||||
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
|
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
|
@ -15,7 +15,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
|
|||||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResyncChartsProcessorService {
|
export class ResyncChartsProcessorService {
|
||||||
@ -43,7 +43,7 @@ export class ResyncChartsProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<Record<string, unknown>>, done: () => void): Promise<void> {
|
public async process(): Promise<void> {
|
||||||
this.logger.info('Resync charts...');
|
this.logger.info('Resync charts...');
|
||||||
|
|
||||||
// TODO: ユーザーごとのチャートも更新する
|
// TODO: ユーザーごとのチャートも更新する
|
||||||
@ -55,6 +55,5 @@ export class ResyncChartsProcessorService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
this.logger.succ('All charts successfully resynced.');
|
this.logger.succ('All charts successfully resynced.');
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user