merge: upstream changes (#123)

This commit is contained in:
Marie 2023-10-31 21:31:41 +01:00 committed by GitHub
commit c44b261bef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
219 changed files with 6837 additions and 2283 deletions

View file

@ -1,60 +0,0 @@
---
name: 🐛 Bug Report
about: Create a report to help us improve
title: ''
labels: ⚠bug?
assignees: ''
---
<!--
Thanks for reporting!
First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported.
Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first.
-->
## 💡 Summary
<!-- Tell us what the bug is -->
## 🥰 Expected Behavior
<!--- Tell us what should happen -->
## 🤬 Actual Behavior
<!--
Tell us what happens instead of the expected behavior.
Please include errors from the developer console and/or server log files if you have access to them.
-->
## 📝 Steps to Reproduce
1.
2.
3.
## 📌 Environment
<!-- Tell us where on the platform it happens -->
<!-- DO NOT WRITE "latest". Please provide the specific version. -->
### 💻 Frontend
* Model and OS of the device(s):
<!-- Example: MacBook Pro (14inch, 2021), macOS Ventura 13.4 -->
* Browser:
<!-- Example: Chrome 113.0.5672.126 -->
* Server URL:
<!-- Example: misskey.io -->
* Misskey:
13.x.x
### 🛰 Backend (for server 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: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->

View file

@ -0,0 +1,91 @@
name: 🐛 Bug Report
description: Create a report to help us improve
labels: ["⚠bug?"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting!
First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported.
Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first.
- type: textarea
attributes:
label: 💡 Summary
description: Tell us what the bug is
validations:
required: true
- type: textarea
attributes:
label: 🥰 Expected Behavior
description: Tell us what should happen
validations:
required: true
- type: textarea
attributes:
label: 🤬 Actual Behavior
description: |
Tell us what happens instead of the expected behavior.
Please include errors from the developer console and/or server log files if you have access to them.
validations:
required: true
- type: textarea
attributes:
label: 📝 Steps to Reproduce
placeholder: |
1.
2.
3.
validations:
required: false
- type: textarea
attributes:
label: 💻 Frontend Environment
description: |
Tell us where on the platform it happens
DO NOT WRITE "latest". Please provide the specific version.
Examples:
* Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
* Browser: Chrome 113.0.5672.126
* Server URL: misskey.io
* Misskey: 13.x.x
value: |
* Model and OS of the device(s):
* Browser:
* Server URL:
* Misskey:
render: markdown
validations:
required: false
- type: textarea
attributes:
label: 🛰 Backend Environment (for server admin)
description: |
Tell us where on the platform it happens
DO NOT WRITE "latest". Please provide the specific version.
If you are using a managed service, put that after the version.
Examples:
* Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
* Misskey: 13.x.x
* Node: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x
* OS and Architecture: Ubuntu 22.04.2 LTS aarch64
value: |
* Installation Method or Hosting Service:
* Misskey:
* Node:
* PostgreSQL:
* Redis:
* OS and Architecture:
render: markdown
validations:
required: false

View file

@ -1,12 +0,0 @@
---
name: ✨ Feature Request
about: Suggest an idea for this project
title: ''
labels: ✨Feature
assignees: ''
---
## Summary
<!-- Tell us what the suggestion is -->

View file

@ -0,0 +1,11 @@
name: ✨ Feature Request
description: Suggest an idea for this project
labels: ["✨Feature"]
body:
- type: textarea
attributes:
label: Summary
description: Tell us what the suggestion is
validations:
required: true

186
.github/workflows/get-api-diff.yml vendored Normal file
View file

@ -0,0 +1,186 @@
# this name is used in report-api-diff.yml so be careful when change name
name: Get api.json from Misskey
on:
pull_request:
branches:
- master
- develop
jobs:
get-base:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node-version: [20.5.1]
services:
db:
image: postgres:13
ports:
- 5432:5432
env:
POSTGRES_DB: misskey
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_USER: example-misskey-user
POSTGRESS_PASS: example-misskey-pass
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4.1.1
with:
repository: ${{ github.event.pull_request.base.repo.full_name }}
ref: ${{ github.base_ref }}
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .config/example.yml .config/default.yml
- name: Build
run: pnpm build
- name : Migrate
run: pnpm migrate
- name: Launch misskey
run: |
screen -S misskey -dm pnpm run dev
sleep 30s
- name: Wait for Misskey to be ready
run: |
MAX_RETRIES=12
RETRY_DELAY=5
count=0
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
printf '.'
sleep $RETRY_DELAY
count=$((count + 1))
done
if [[ $count -eq $MAX_RETRIES ]]; then
echo "Failed to connect to Misskey after $MAX_RETRIES attempts."
exit 1
fi
- id: fetch
name: Get api.json from Misskey
run: |
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
echo $RESULT > api-base.json
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: api-artifact
path: api-base.json
- name: Kill Misskey Job
run: screen -S misskey -X quit
get-head:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node-version: [20.5.1]
services:
db:
image: postgres:13
ports:
- 5432:5432
env:
POSTGRES_DB: misskey
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_USER: example-misskey-user
POSTGRESS_PASS: example-misskey-pass
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4.1.1
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.head_ref }}
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .config/example.yml .config/default.yml
- name: Build
run: pnpm build
- name : Migrate
run: pnpm migrate
- name: Launch misskey
run: |
screen -S misskey -dm pnpm run dev
sleep 30s
- name: Wait for Misskey to be ready
run: |
MAX_RETRIES=12
RETRY_DELAY=5
count=0
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
printf '.'
sleep $RETRY_DELAY
count=$((count + 1))
done
if [[ $count -eq $MAX_RETRIES ]]; then
echo "Failed to connect to Misskey after $MAX_RETRIES attempts."
exit 1
fi
- id: fetch
name: Get api.json from Misskey
run: |
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
echo $RESULT > api-head.json
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: api-artifact
path: api-head.json
- name: Kill Misskey Job
run: screen -S misskey -X quit
save-pr-number:
runs-on: ubuntu-latest
steps:
- name: Save PR number
env:
PR_NUMBER: ${{ github.event.number }}
run: |
echo "$PR_NUMBER" > ./pr_number
- uses: actions/upload-artifact@v3
with:
name: api-artifact
path: pr_number

85
.github/workflows/report-api-diff.yml vendored Normal file
View file

@ -0,0 +1,85 @@
name: Report API Diff
on:
workflow_run:
types: [completed]
workflows:
- Get api.json from Misskey # get-api-diff.yml
jobs:
compare-diff:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
pull-requests: write
# api-artifact
steps:
- name: Download artifact
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "api-artifact"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/api-artifact.zip`, Buffer.from(download.data));
- name: Extract artifact
run: unzip api-artifact.zip -d artifacts
- name: Load PR Number
id: load-pr-num
run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
- name: Output base
run: cat ./artifacts/api-base.json
- name: Output head
run: cat ./artifacts/api-head.json
- name: Arrange json files
run: |
jq '.' ./artifacts/api-base.json > ./api-base.json
jq '.' ./artifacts/api-head.json > ./api-head.json
- name: Get diff of 2 files
run: diff -u --label=base --label=head ./api-base.json ./api-head.json | cat > api.json.diff
- name: Get full diff
run: diff --label=base --label=head --new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L' ./api-base.json ./api-head.json | cat > api-full.json.diff
- name: Echo full diff
run: cat ./api-full.json.diff
- name: Upload full diff to Artifact
uses: actions/upload-artifact@v3
with:
name: api-artifact
path: |
api-full.json.diff
api-base.json
api-head.json
- id: out-diff
name: Build diff Comment
run: |
cat <<- EOF > ./output.md
このPRによるapi.jsonの差分
<details>
<summary>差分はこちら</summary>
\`\`\`diff
$(cat ./api.json.diff)
\`\`\`
</details>
[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})
EOF
- uses: thollander/actions-comment-pull-request@v2
with:
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
comment_tag: show_diff
filePath: ./output.md

View file

@ -12,25 +12,81 @@
--> -->
## 2023.x.x (unreleased) ## 2023.11.0 (unreleased)
### General
- Feat: アイコンデコレーション機能
- サーバーで用意された画像をアイコンに重ねることができます
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
- 画像は512x512pxを推奨します。
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
### Client
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
- Enhance: スワイプしてタイムラインを再読込できるように
- PCの場合は右上のボタンからでも再読込できます
- Enhance: タイムラインの自動更新を無効にできるように
- Enhance: コードのシンタックスハイライトエンジンをShikiに変更
- AiScriptのシンタックスハイライトに対応
- MFMでAiScriptをハイライトする場合、コードブロックの開始部分を ` ```is ` もしくは ` ```aiscript ` としてください
- Enhance: データセーバー有効時はアニメーション付きのアバター画像が停止するように
- Enhance: プラグインを削除した際には、使用されていたアクセストークンも同時に削除されるようになりました
- Enhance: プラグインで`Plugin:register_note_view_interruptor`を用いてnoteの代わりにnullを返却することでートを非表示にできるようになりました
- Enhance: AiScript関数`Mk:nyaize()`が追加されました
- Enhance: 情報→ツール はナビゲーションバーにツールとして独立した項目になりました
- Enhance: その他細かなブラッシュアップ
- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正
- Fix: ユーザーページの ノート > ファイル付き タブにリプライが表示されてしまう
- Fix: 「検索」MFMにおいて一部の検索キーワードが正しく認識されない問題を修正
- Fix: 一部の言語でMisskey Webがクラッシュする問題を修正
- Fix: チャンネルの作成・更新時に失敗した場合何も表示されない問題を修正 #11983
- Fix: 個人カードのemojiがバッテリーになっている問題を修正
- Fix: 標準テーマと同じIDを使用してインストールできてしまう問題を修正
### Server
- Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように
- Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善
- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました
- 相手がMisskey v2023.11.0以降である必要があります
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正
- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正
- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正
- Fix: STLでフォローしていないチャンネルが取得される問題を修正
- Fix: `hashtags/trend`にてRedisからトレンドの情報が取得できない際にInternal Server Errorになる問題を修正
- Fix: HTLをリロードまたは遡行したとき、フォローしているチャンネルのートが含まれない問題を修正 #11765 #12181
- Fix: リノートをリノートできるのを修正
- Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正
- Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正
## 2023.10.2
### General ### General
- Feat: アンテナでローカルの投稿のみ収集できるようになりました - Feat: アンテナでローカルの投稿のみ収集できるようになりました
- Feat: サーバーサイレンス機能が追加されました - Feat: サーバーサイレンス機能が追加されました
- Enhance: 新規にフォローした人の返信をデフォルトでTLに追加できるオプションを追加 - Enhance: 新規にフォローした人の返信をデフォルトでTLに追加できるオプションを追加
- Enhance: HTLとLTLを2023.10.0アップデート以前まで遡れるように - Enhance: HTL/LTL/STLを2023.10.0アップデート以前まで遡れるように
- Enhance: フォロー/フォロー解除したときに過去分のHTLにも含まれる投稿が反映されるように - Enhance: フォロー/フォロー解除したときに過去分のHTLにも含まれる投稿が反映されるように
- Enhance: ローカリゼーションの更新 - Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新 - Enhance: 依存関係の更新
### Client ### Client
- Enhance: TLの返信表示オプションを記憶するように - Enhance: TLの返信表示オプションを記憶するように
- Enhance: 投稿されてから時間が経過しているノートであることを視覚的に分かりやすく
### Server ### Server
- Enhance: タイムライン取得時のパフォーマンスを向上
- Enhance: ストリーミングAPIのパフォーマンスを向上 - Enhance: ストリーミングAPIのパフォーマンスを向上
- Fix: users/notesでDBから参照した際にチャンネル投稿のみ取得される問題を修正 - Fix: users/notesでDBから参照した際にチャンネル投稿のみ取得される問題を修正
- Fix: コントロールパネルの設定項目が正しく保存できない問題を修正 - Fix: コントロールパネルの設定項目が正しく保存できない問題を修正
- Change: nyaizeはAPIレスポンス時ではなく投稿時に一度だけ非可逆的に行われるようになりました - Fix: 管理者権限のロールを持っていても一部のAPIが使用できないことがある問題を修正
- Change: ユーザーのisCatがtrueでも、サーバーではnyaizeが行われなくなりました
- isCatな場合、クライアントでnyaize処理を行うことを推奨します
## 2023.10.1 ## 2023.10.1
### General ### General

View file

@ -1,3 +1,4 @@
/* flaky
describe('After user signed in', () => { describe('After user signed in', () => {
beforeEach(() => { beforeEach(() => {
cy.resetState(); cy.resetState();
@ -67,3 +68,4 @@ describe('After user signed in', () => {
buildWidgetTest('aiscript'); buildWidgetTest('aiscript');
buildWidgetTest('aichan'); buildWidgetTest('aichan');
}); });
*/

View file

@ -999,6 +999,7 @@ expired: "منتهية صلاحيته"
icon: "الصورة الرمزية" icon: "الصورة الرمزية"
replies: "رد" replies: "رد"
renotes: "أعد النشر" renotes: "أعد النشر"
flip: "اقلب"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "نجح إنشاء حسابك!" accountCreated: "نجح إنشاء حسابك!"
letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي." letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي."

View file

@ -840,6 +840,7 @@ youFollowing: "অনুসরণ করা হচ্ছে"
icon: "প্রোফাইল ছবি" icon: "প্রোফাইল ছবি"
replies: "জবাব" replies: "জবাব"
renotes: "রিনোট" renotes: "রিনোট"
flip: "উল্টান"
_role: _role:
priority: "অগ্রাধিকার" priority: "অগ্রাধিকার"
_priority: _priority:

View file

@ -1096,6 +1096,7 @@ iHaveReadXCarefullyAndAgree: "Přečetl jsem si text \"{x}\" a souhlasím s ním
icon: "Avatar" icon: "Avatar"
replies: "Odpovědi" replies: "Odpovědi"
renotes: "Přeposlat" renotes: "Přeposlat"
flip: "Otočit"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Váš účet byl úspěšně vytvořen!" accountCreated: "Váš účet byl úspěšně vytvořen!"
letsStartAccountSetup: "Pro začátek si nastavte svůj profil." letsStartAccountSetup: "Pro začátek si nastavte svůj profil."

View file

@ -979,6 +979,7 @@ assign: "Zuweisen"
unassign: "Entfernen" unassign: "Entfernen"
color: "Farbe" color: "Farbe"
manageCustomEmojis: "Kann benutzerdefinierte Emojis verwalten" manageCustomEmojis: "Kann benutzerdefinierte Emojis verwalten"
manageAvatarDecorations: "Profilbilddekorationen verwalten"
youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht." youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht."
cannotPerformTemporary: "Vorübergehend nicht verfügbar" cannotPerformTemporary: "Vorübergehend nicht verfügbar"
cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut." cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut."
@ -1132,6 +1133,10 @@ mutualFollow: "Gegenseitig gefolgt"
fileAttachedOnly: "Nur Notizen mit Dateien" fileAttachedOnly: "Nur Notizen mit Dateien"
showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen" showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen"
hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen" hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen"
showRepliesToOthersInTimelineAll: "Antworten von allen momentan gefolgten Benutzern in Chronik anzeigen"
hideRepliesToOthersInTimelineAll: "Antworten von allen momentan gefolgten Benutzern nicht in Chronik anzeigen"
confirmShowRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern in der Chronik anzeigen?"
confirmHideRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern nicht in der Chronik anzeigen?"
externalServices: "Externe Dienste" externalServices: "Externe Dienste"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Impressums-URL" impressumUrl: "Impressums-URL"
@ -1139,6 +1144,16 @@ impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung,
privacyPolicy: "Datenschutzerklärung" privacyPolicy: "Datenschutzerklärung"
privacyPolicyUrl: "Datenschutzerklärungs-URL" privacyPolicyUrl: "Datenschutzerklärungs-URL"
tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung" tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung"
avatarDecorations: "Profilbilddekoration"
attach: "Anbringen"
detach: "Entfernen"
angle: "Winkel"
flip: "Umdrehen"
showAvatarDecorations: "Profilbilddekoration anzeigen"
releaseToRefresh: "Zum Aktualisieren loslassen"
refreshing: "Wird aktualisiert..."
pullDownToRefresh: "Zum Aktualisieren ziehen"
disableStreamingTimeline: "Echtzeitaktualisierung der Chronik deaktivieren"
_announcement: _announcement:
forExistingUsers: "Nur für existierende Nutzer" forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
@ -1174,6 +1189,7 @@ _serverSettings:
manifestJsonOverride: "Überschreiben von manifest.json" manifestJsonOverride: "Überschreiben von manifest.json"
shortName: "Abkürzung" shortName: "Abkürzung"
shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist." shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist."
fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden."
_accountMigration: _accountMigration:
moveFrom: "Von einem anderen Konto zu diesem migrieren" moveFrom: "Von einem anderen Konto zu diesem migrieren"
moveFromSub: "Alias für ein anderes Konto erstellen" moveFromSub: "Alias für ein anderes Konto erstellen"
@ -1474,6 +1490,7 @@ _role:
inviteLimitCycle: "Zyklus des Einladungslimits" inviteLimitCycle: "Zyklus des Einladungslimits"
inviteExpirationTime: "Gültigkeitsdauer von Einladungen" inviteExpirationTime: "Gültigkeitsdauer von Einladungen"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
canManageAvatarDecorations: "Profilbilddekorationen verwalten"
driveCapacity: "Drive-Kapazität" driveCapacity: "Drive-Kapazität"
alwaysMarkNsfw: "Dateien immer als NSFW markieren" alwaysMarkNsfw: "Dateien immer als NSFW markieren"
pinMax: "Maximale Anzahl an angehefteten Notizen" pinMax: "Maximale Anzahl an angehefteten Notizen"
@ -1593,6 +1610,7 @@ _aboutMisskey:
donate: "An Sharkey spenden" donate: "An Sharkey spenden"
morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰" morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰"
patrons: "UnterstützerInnen" patrons: "UnterstützerInnen"
projectMembers: "Projektmitglieder"
_displayOfSensitiveMedia: _displayOfSensitiveMedia:
respect: "Sensible Medien verbergen" respect: "Sensible Medien verbergen"
ignore: "Sensible Medien anzeigen" ignore: "Sensible Medien anzeigen"
@ -2146,6 +2164,9 @@ _moderationLogTypes:
createAd: "Werbung erstellt" createAd: "Werbung erstellt"
deleteAd: "Werbung gelöscht" deleteAd: "Werbung gelöscht"
updateAd: "Werbung aktualisiert" updateAd: "Werbung aktualisiert"
createAvatarDecoration: "Profilbilddekoration erstellt"
updateAvatarDecoration: "Profilbilddekoration aktualisiert"
deleteAvatarDecoration: "Profilbilddekoration gelöscht"
_fileViewer: _fileViewer:
title: "Dateiinformationen" title: "Dateiinformationen"
type: "Dateityp" type: "Dateityp"
@ -2154,3 +2175,44 @@ _fileViewer:
uploadedAt: "Hochgeladen am" uploadedAt: "Hochgeladen am"
attachedNotes: "Zugehörige Notizen" attachedNotes: "Zugehörige Notizen"
thisPageCanBeSeenFromTheAuthor: "Nur der Benutzer, der diese Datei hochgeladen hat, kann diese Seite sehen." thisPageCanBeSeenFromTheAuthor: "Nur der Benutzer, der diese Datei hochgeladen hat, kann diese Seite sehen."
_externalResourceInstaller:
title: "Von externer Seite installieren"
checkVendorBeforeInstall: "Überprüfe vor Installation die Vertrauenswürdigkeit des Vertreibers."
_plugin:
title: "Möchtest du dieses Plugin installieren?"
metaTitle: "Plugininformation"
_theme:
title: "Möchten du dieses Farbschema installieren?"
metaTitle: "Farbschemainfo"
_meta:
base: "Farbschemavorlage"
_vendorInfo:
title: "Vertreiber"
endpoint: "Referenzierter Endpunkt"
hashVerify: "Hash-Verifikation"
_errors:
_invalidParams:
title: "Ungültige Parameter"
description: "Es fehlen Informationen zum Laden der externen Ressource. Überprüfe die übergebene URL."
_resourceTypeNotSupported:
title: "Diese Ressource wird nicht unterstützt"
description: "Dieser Ressourcentyp wird nicht unterstützt. Bitte kontaktiere den Seitenbesitzer."
_failedToFetch:
title: "Fehler beim Abrufen der Daten"
fetchErrorDescription: "Während der Kommunikation mit der externen Seite ist ein Fehler aufgetreten. Kontaktiere den Seitenbesitzer, falls ein erneutes Probieren dieses Problem nicht löst."
parseErrorDescription: "Während dem Auslesen der externen Daten ist ein Fehler aufgetreten. Kontaktiere den Seitenbesitzer."
_hashUnmatched:
title: "Datenverifizierung fehlgeschlagen"
description: "Die Integritätsprüfung der geladenen Daten ist fehlgeschlagen. Aus Sicherheitsgründen kann die Installation nicht fortgesetzt werden. Kontaktiere den Seitenbesitzer."
_pluginParseFailed:
title: "AiScript-Fehler"
description: "Die angeforderten Daten wurden erfolgreich abgerufen, jedoch trat während des AiScript-Parsings ein Fehler auf. Kontaktiere den Autor des Plugins. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
_pluginInstallFailed:
title: "Das Plugin konnte nicht installiert werden"
description: "Während der Installation des Plugin ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
_themeParseFailed:
title: "Parsing des Farbschemas fehlgeschlagen"
description: "Die angeforderten Daten wurden erfolgreich abgerufen, jedoch trat während des Farbschema-Parsings ein Fehler auf. Kontaktiere den Autor des Farbschemas. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
_themeInstallFailed:
title: "Das Farbschema konnte nicht installiert werden"
description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."

View file

@ -606,7 +606,7 @@ poll: "Poll"
useCw: "Hide content" useCw: "Hide content"
enablePlayer: "Open video player" enablePlayer: "Open video player"
disablePlayer: "Close video player" disablePlayer: "Close video player"
expandTweet: "Expand tweet" expandTweet: "Expand post"
themeEditor: "Theme editor" themeEditor: "Theme editor"
description: "Description" description: "Description"
describeFile: "Add caption" describeFile: "Add caption"
@ -999,6 +999,7 @@ assign: "Assign"
unassign: "Unassign" unassign: "Unassign"
color: "Color" color: "Color"
manageCustomEmojis: "Manage Custom Emojis" manageCustomEmojis: "Manage Custom Emojis"
manageAvatarDecorations: "Manage avatar decorations"
youCannotCreateAnymore: "You've hit the creation limit." youCannotCreateAnymore: "You've hit the creation limit."
cannotPerformTemporary: "Temporarily unavailable" cannotPerformTemporary: "Temporarily unavailable"
cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again." cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again."
@ -1161,6 +1162,10 @@ mutualFollow: "Mutual follow"
fileAttachedOnly: "Only notes with files" fileAttachedOnly: "Only notes with files"
showRepliesToOthersInTimeline: "Show replies to others in timeline" showRepliesToOthersInTimeline: "Show replies to others in timeline"
hideRepliesToOthersInTimeline: "Hide replies to others from timeline" hideRepliesToOthersInTimeline: "Hide replies to others from timeline"
showRepliesToOthersInTimelineAll: "Show replies to others from everyone you follow in timeline"
hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you follow in timeline"
confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?"
confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?"
externalServices: "External Services" externalServices: "External Services"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Impressum URL" impressumUrl: "Impressum URL"
@ -1168,6 +1173,16 @@ impressumDescription: "In some countries, like germany, the inclusion of operato
privacyPolicy: "Privacy Policy" privacyPolicy: "Privacy Policy"
privacyPolicyUrl: "Privacy Policy URL" privacyPolicyUrl: "Privacy Policy URL"
tosAndPrivacyPolicy: "Terms of Service and Privacy Policy" tosAndPrivacyPolicy: "Terms of Service and Privacy Policy"
avatarDecorations: "Avatar decorations"
attach: "Attach"
detach: "Remove"
angle: "Angle"
flip: "Flip"
showAvatarDecorations: "Show avatar decorations"
releaseToRefresh: "Release to refresh"
refreshing: "Refreshing..."
pullDownToRefresh: "Pull down to refresh"
disableStreamingTimeline: "Disable real-time timeline updates"
_announcement: _announcement:
forExistingUsers: "Existing users only" forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1203,6 +1218,7 @@ _serverSettings:
manifestJsonOverride: "manifest.json Override" manifestJsonOverride: "manifest.json Override"
shortName: "Short name" shortName: "Short name"
shortNameDescription: "A shorthand for the instance's name that can be displayed if the full official name is long." shortNameDescription: "A shorthand for the instance's name that can be displayed if the full official name is long."
fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability."
_accountMigration: _accountMigration:
moveFrom: "Migrate another account to this one" moveFrom: "Migrate another account to this one"
moveFromSub: "Create alias to another account" moveFromSub: "Create alias to another account"
@ -1503,6 +1519,7 @@ _role:
inviteLimitCycle: "Invite limit cooldown" inviteLimitCycle: "Invite limit cooldown"
inviteExpirationTime: "Invite expiration interval" inviteExpirationTime: "Invite expiration interval"
canManageCustomEmojis: "Can manage custom emojis" canManageCustomEmojis: "Can manage custom emojis"
canManageAvatarDecorations: "Manage avatar decorations"
driveCapacity: "Drive capacity" driveCapacity: "Drive capacity"
alwaysMarkNsfw: "Always mark files as NSFW" alwaysMarkNsfw: "Always mark files as NSFW"
pinMax: "Maximum number of pinned notes" pinMax: "Maximum number of pinned notes"
@ -1624,6 +1641,7 @@ _aboutMisskey:
donate: "Donate to Sharkey" donate: "Donate to Sharkey"
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
patrons: "Patrons" patrons: "Patrons"
projectMembers: "Project members"
_displayOfSensitiveMedia: _displayOfSensitiveMedia:
respect: "Hide media marked as sensitive" respect: "Hide media marked as sensitive"
ignore: "Display media marked as sensitive" ignore: "Display media marked as sensitive"
@ -2179,6 +2197,9 @@ _moderationLogTypes:
createAd: "Ad created" createAd: "Ad created"
deleteAd: "Ad deleted" deleteAd: "Ad deleted"
updateAd: "Ad updated" updateAd: "Ad updated"
createAvatarDecoration: "Avatar decoration created"
updateAvatarDecoration: "Avatar decoration updated"
deleteAvatarDecoration: "Avatar decoration deleted"
_mfm: _mfm:
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax." intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
dummy: "Sharkey expands the world of the Fediverse" dummy: "Sharkey expands the world of the Fediverse"
@ -2260,3 +2281,44 @@ _fileViewer:
uploadedAt: "Uploaded at" uploadedAt: "Uploaded at"
attachedNotes: "Attached notes" attachedNotes: "Attached notes"
thisPageCanBeSeenFromTheAuthor: "This page can only be seen by the user who uploaded this file." thisPageCanBeSeenFromTheAuthor: "This page can only be seen by the user who uploaded this file."
_externalResourceInstaller:
title: "Install from external site"
checkVendorBeforeInstall: "Make sure the distributor of this resource is trustworthy before installation."
_plugin:
title: "Do you want to install this plugin?"
metaTitle: "Plugin information"
_theme:
title: "Do you want to install this theme?"
metaTitle: "Theme information"
_meta:
base: "Base color scheme"
_vendorInfo:
title: "Distributor information"
endpoint: "Referenced endpoint"
hashVerify: "Hash verification"
_errors:
_invalidParams:
title: "Invalid parameters"
description: "There is not enough information to load data from an external site. Please confirm the entered URL."
_resourceTypeNotSupported:
title: "This external resource is not supported"
description: "The type of this external resource is not supported. Please contact the site administrator."
_failedToFetch:
title: "Failed to fetch data"
fetchErrorDescription: "An error occurred communicating with the external site. If trying again does not fix this issue, please contact the site administrator."
parseErrorDescription: "An error occurred processing the data loaded from the external site. Please contact the site administrator."
_hashUnmatched:
title: "Data verification failed"
description: "An error occurred verifying the integrity of the fetched data. As a security measure, installation cannot continue. Please contact the site administrator."
_pluginParseFailed:
title: "AiScript Error"
description: "The requested data was fetched successfully, but an error occurred during AiScript parsing. Please contact the plugin author. Error details can be viewed in the Javascript console."
_pluginInstallFailed:
title: "Plugin installation failed"
description: "A problem occurred during plugin installation. Please try again. Error details can be viewed in the Javascript console."
_themeParseFailed:
title: "Theme parsing failed"
description: "The requested data was fetched successfully, but an error occurred during theme parsing. Please contact the theme author. Error details can be viewed in the Javascript console."
_themeInstallFailed:
title: "Failed to install theme"
description: "A problem occurred during theme installation. Please try again. Error details can be viewed in the Javascript console."

View file

@ -1139,6 +1139,7 @@ impressumDescription: "En algunos países, como Alemania, la inclusión del oper
privacyPolicy: "Política de Privacidad" privacyPolicy: "Política de Privacidad"
privacyPolicyUrl: "URL de la Política de Privacidad" privacyPolicyUrl: "URL de la Política de Privacidad"
tosAndPrivacyPolicy: "Condiciones de Uso y Política de Privacidad" tosAndPrivacyPolicy: "Condiciones de Uso y Política de Privacidad"
flip: "Echar de un capirotazo"
_announcement: _announcement:
forExistingUsers: "Solo para usuarios registrados" forExistingUsers: "Solo para usuarios registrados"
forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán." forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán."

View file

@ -387,7 +387,7 @@ antennaSource: "Source de lantenne"
antennaKeywords: "Mots clés à recevoir" antennaKeywords: "Mots clés à recevoir"
antennaExcludeKeywords: "Mots clés à exclure" antennaExcludeKeywords: "Mots clés à exclure"
antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR."
notifyAntenna: "Je souhaite recevoir les notifications des nouvelles notes" notifyAntenna: "Me notifier pour les nouvelles notes"
withFileAntenna: "Notes ayant des attachements uniquement" withFileAntenna: "Notes ayant des attachements uniquement"
enableServiceworker: "Activer ServiceWorker" enableServiceworker: "Activer ServiceWorker"
antennaUsersDescription: "Saisissez un seul nom dutilisateur·rice par ligne" antennaUsersDescription: "Saisissez un seul nom dutilisateur·rice par ligne"
@ -528,6 +528,7 @@ objectStorageSetPublicRead: "Régler sur « public » lors de l'envoi"
serverLogs: "Journal du serveur" serverLogs: "Journal du serveur"
deleteAll: "Supprimer tout" deleteAll: "Supprimer tout"
showFixedPostForm: "Afficher le formulaire de publication en haut du fil d'actualité" showFixedPostForm: "Afficher le formulaire de publication en haut du fil d'actualité"
withRepliesByDefaultForNewlyFollowed: "Afficher les réponses des nouvelles personnes que vous suivez dans le fil par défaut"
newNoteRecived: "Voir les nouvelles notes" newNoteRecived: "Voir les nouvelles notes"
sounds: "Sons" sounds: "Sons"
sound: "Sons" sound: "Sons"
@ -610,7 +611,7 @@ permission: "Autorisations "
enableAll: "Tout activer" enableAll: "Tout activer"
disableAll: "Tout désactiver" disableAll: "Tout désactiver"
tokenRequested: "Autoriser l'accès au compte" tokenRequested: "Autoriser l'accès au compte"
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." pluginTokenRequestedDescription: "Cette extension pourra utiliser les autorisations définies ici."
notificationType: "Type de notifications" notificationType: "Type de notifications"
edit: "Editer" edit: "Editer"
emailServer: "Serveur de messagerie" emailServer: "Serveur de messagerie"
@ -692,7 +693,7 @@ repliesCount: "Nombre de réponses envoyées"
renotesCount: "Nombre de notes que vous avez renotées" renotesCount: "Nombre de notes que vous avez renotées"
repliedCount: "Nombre de réponses reçues" repliedCount: "Nombre de réponses reçues"
renotedCount: "Nombre de vos notes renotées" renotedCount: "Nombre de vos notes renotées"
followingCount: "Nombre de comptes suivis" followingCount: "Nombre d'abonnements"
followersCount: "Nombre d'abonnés" followersCount: "Nombre d'abonnés"
sentReactionsCount: "Nombre de réactions envoyées" sentReactionsCount: "Nombre de réactions envoyées"
receivedReactionsCount: "Nombre de réactions reçues" receivedReactionsCount: "Nombre de réactions reçues"
@ -780,7 +781,7 @@ addDescription: "Ajouter une description"
userPagePinTip: "Vous pouvez afficher des notes ici en sélectionnant l'option « Épingler au profil » dans le menu de chaque note." userPagePinTip: "Vous pouvez afficher des notes ici en sélectionnant l'option « Épingler au profil » dans le menu de chaque note."
notSpecifiedMentionWarning: "Vous avez mentionné des utilisateur·rice·s qui ne font pas partie de la liste des destinataires" notSpecifiedMentionWarning: "Vous avez mentionné des utilisateur·rice·s qui ne font pas partie de la liste des destinataires"
info: "Informations" info: "Informations"
userInfo: "Informations sur l'utilisateur" userInfo: "Informations sur l'utilisateur·rice"
unknown: "Inconnu" unknown: "Inconnu"
onlineStatus: "Statut" onlineStatus: "Statut"
hideOnlineStatus: "Se rendre invisible" hideOnlineStatus: "Se rendre invisible"
@ -969,9 +970,11 @@ assign: "Attribuer"
unassign: "Retirer" unassign: "Retirer"
color: "Couleur" color: "Couleur"
manageCustomEmojis: "Gestion des émojis personnalisés" manageCustomEmojis: "Gestion des émojis personnalisés"
manageAvatarDecorations: "Gérer les décorations d'avatar"
youCannotCreateAnymore: "Vous avez atteint la limite de création." youCannotCreateAnymore: "Vous avez atteint la limite de création."
cannotPerformTemporary: "Temporairement indisponible" cannotPerformTemporary: "Temporairement indisponible"
invalidParamError: "Paramètres invalides" invalidParamError: "Paramètres invalides"
permissionDeniedError: "Opération refusée"
preset: "Préréglage" preset: "Préréglage"
selectFromPresets: "Sélectionner à partir des préréglages" selectFromPresets: "Sélectionner à partir des préréglages"
achievements: "Accomplissements" achievements: "Accomplissements"
@ -985,11 +988,13 @@ internalServerError: "Erreur interne du serveur"
copyErrorInfo: "Copier les détails de lerreur" copyErrorInfo: "Copier les détails de lerreur"
exploreOtherServers: "Trouver une autre instance" exploreOtherServers: "Trouver une autre instance"
disableFederationOk: "Désactiver" disableFederationOk: "Désactiver"
postToTheChannel: "Publier au canal"
likeOnly: "Les favoris uniquement" likeOnly: "Les favoris uniquement"
sensitiveWords: "Mots sensibles" sensitiveWords: "Mots sensibles"
notesSearchNotAvailable: "La recherche de notes n'est pas disponible." notesSearchNotAvailable: "La recherche de notes n'est pas disponible."
license: "Licence" license: "Licence"
myClips: "Mes clips" myClips: "Mes clips"
retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur."
showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note" showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note"
noteIdOrUrl: "Identifiant de la note ou URL" noteIdOrUrl: "Identifiant de la note ou URL"
video: "Vidéo" video: "Vidéo"
@ -1013,21 +1018,34 @@ vertical: "Vertical"
horizontal: "Latéral" horizontal: "Latéral"
position: "Position" position: "Position"
serverRules: "Règles du serveur" serverRules: "Règles du serveur"
preservedUsernames: "Nom d'utilisateur·rice réservé" pleaseAgreeAllToContinue: "Pour continuer, veuillez accepter tous les champs ci-dessus."
continue: "Continuer"
preservedUsernames: "Noms d'utilisateur·rice réservés"
archive: "Archive" archive: "Archive"
displayOfNote: "Affichage de la note" displayOfNote: "Affichage de la note"
initialAccountSetting: "Configuration initiale du profil"
youFollowing: "Abonné·e" youFollowing: "Abonné·e"
preventAiLearning: "Refuser l'usage dans l'apprentissage automatique d'IA générative"
preventAiLearningDescription: "Demander aux robots d'indexation de ne pas utiliser le contenu publié, tel que les notes et les images, dans l'apprentissage automatique d'IA générative. Cela est réalisé en incluant le drapeau « noai » dans la réponse HTML. Une prévention complète n'est toutefois pas possible, car il est au robot d'indexation de respecter cette demande."
options: "Options" options: "Options"
specifyUser: "Spécifier l'utilisateur·rice"
failedToPreviewUrl: "Aperçu d'URL échoué"
update: "Mettre à jour"
later: "Plus tard" later: "Plus tard"
goToMisskey: "Retour vers Misskey" goToMisskey: "Retour vers Misskey"
expirationDate: "Date dexpiration" expirationDate: "Date dexpiration"
waitingForMailAuth: "En attente de la vérification de l'adresse courriel"
usedAt: "Utilisé le" usedAt: "Utilisé le"
unused: "Non-utilisé" unused: "Non-utilisé"
used: "Utilisé" used: "Utilisé"
expired: "Expiré" expired: "Expiré"
doYouAgree: "Êtes-vous daccord ?" doYouAgree: "Êtes-vous daccord ?"
beSureToReadThisAsItIsImportant: "Assurez-vous de le lire; c'est important."
dialog: "Dialogue"
icon: "Avatar" icon: "Avatar"
forYou: "Pour vous" forYou: "Pour vous"
currentAnnouncements: "Annonces actuelles"
pastAnnouncements: "Annonces passées"
replies: "Réponses" replies: "Réponses"
renotes: "Renotes" renotes: "Renotes"
loadReplies: "Inclure les réponses" loadReplies: "Inclure les réponses"
@ -1035,7 +1053,34 @@ pinnedList: "Liste épinglée"
notifyNotes: "Notifier à propos des nouvelles notes" notifyNotes: "Notifier à propos des nouvelles notes"
authentication: "Authentification" authentication: "Authentification"
authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer" authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer"
dateAndTime: "Date et heure"
showRenotes: "Afficher les renotes" showRenotes: "Afficher les renotes"
edited: "Modifié"
notificationRecieveConfig: "Paramètres des notifications"
mutualFollow: "Abonnement mutuel"
showRepliesToOthersInTimeline: "Afficher les réponses aux autres dans le fil"
hideRepliesToOthersInTimeline: "Masquer les réponses aux autres dans le fil"
showRepliesToOthersInTimelineAll: "Afficher les réponses de toutes les personnes que vous suivez dans le fil"
hideRepliesToOthersInTimelineAll: "Masquer les réponses de toutes les personnes que vous suivez dans le fil"
confirmShowRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment afficher les réponses de toutes les personnes que vous suivez dans le fil ?"
confirmHideRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment masquer les réponses de toutes les personnes que vous suivez dans le fil ?"
externalServices: "Services externes"
impressum: "Impressum"
impressumUrl: "URL de l'impressum"
impressumDescription: "Dans certains pays comme l'Allemagne, il est obligatoire d'afficher les informations sur l'opérateur d'un site (un impressum)."
privacyPolicy: "Politique de confidentialité"
privacyPolicyUrl: "URL de la politique de confidentialité"
tosAndPrivacyPolicy: "Conditions d'utilisation et politique de confidentialité"
avatarDecorations: "Décorations d'avatar"
attach: "Mettre"
detach: "Enlever"
angle: "Angle"
flip: "Inverser"
showAvatarDecorations: "Afficher les décorations d'avatar"
releaseToRefresh: "Relâcher pour rafraîchir"
refreshing: "Rafraîchissement..."
pullDownToRefresh: "Tirer vers le bas pour rafraîchir"
disableStreamingTimeline: "Désactiver les mises à jour en temps réel de la ligne du temps"
_announcement: _announcement:
readConfirmTitle: "Marquer comme lu ?" readConfirmTitle: "Marquer comme lu ?"
_initialAccountSetting: _initialAccountSetting:
@ -1043,9 +1088,10 @@ _initialAccountSetting:
privacySetting: "Paramètres de confidentialité" privacySetting: "Paramètres de confidentialité"
initialAccountSettingCompleted: "Configuration du profil terminée avec succès !" initialAccountSettingCompleted: "Configuration du profil terminée avec succès !"
ifYouNeedLearnMore: "Si vous voulez en savoir plus comment utiliser {name}(Misskey), veuillez visiter {link}." ifYouNeedLearnMore: "Si vous voulez en savoir plus comment utiliser {name}(Misskey), veuillez visiter {link}."
skipAreYouSure: "Désirez-vous ignorer la configuration du profile ?" skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?"
_serverSettings: _serverSettings:
iconUrl: "URL de licône" iconUrl: "URL de licône"
fanoutTimelineDescription: "Si activée, la performance de la récupération de la chronologie augmentera considérablement et la charge sur la base de données sera réduite. En revanche, l'utilisation de la mémoire de Redis augmentera. Considérez désactiver cette option si le serveur est bas en mémoire ou instable."
_accountMigration: _accountMigration:
moveFrom: "Migrer un autre compte vers le présent compte" moveFrom: "Migrer un autre compte vers le présent compte"
moveFromSub: "Créer un alias vers un autre compte" moveFromSub: "Créer un alias vers un autre compte"
@ -1112,9 +1158,16 @@ _achievements:
description: "Rendre votre compte comme un chat" description: "Rendre votre compte comme un chat"
flavor: "Je n'ai pas encore de nom" flavor: "Je n'ai pas encore de nom"
_following1: _following1:
title: "Vous suivez votre premier utilisateur·rice" title: "Vous suivez votre premier·ère utilisateur·rice"
_following10:
description: "S'abonner à plus de 10 utilisateur·rice·s"
_following50: _following50:
title: "Beaucoup d'amis" title: "Beaucoup d'amis"
description: "S'abonner à plus de 50 utilisateur·rice·s"
_following100:
description: "S'abonner à plus de 100 utilisateur·rice·s"
_following300:
description: "S'abonner à plus de 300 utilisateur·rice·s"
_followers10: _followers10:
title: "Abonnez-moi !" title: "Abonnez-moi !"
description: "Obtenir plus de 10 abonné·e·s" description: "Obtenir plus de 10 abonné·e·s"
@ -1194,6 +1247,7 @@ _role:
high: "Haute" high: "Haute"
_options: _options:
canManageCustomEmojis: "Gestion des émojis personnalisés" canManageCustomEmojis: "Gestion des émojis personnalisés"
canManageAvatarDecorations: "Gestion des décorations d'avatar"
wordMuteMax: "Nombre maximal de caractères dans le filtre de mots" wordMuteMax: "Nombre maximal de caractères dans le filtre de mots"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement." description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement."
@ -1228,6 +1282,10 @@ _ad:
back: "Retour" back: "Retour"
reduceFrequencyOfThisAd: "Voir cette publicité moins souvent" reduceFrequencyOfThisAd: "Voir cette publicité moins souvent"
hide: "Cacher " hide: "Cacher "
adsSettings: "Paramètres des publicités"
notesPerOneAd: "Intervalle de diffusion de publicités lors de la mise à jour en temps réel (nombre de notes par publicité)"
setZeroToDisable: "Mettre cette valeur à 0 pour désactiver la diffusion de publicités lors de la mise à jour en temps réel"
adsTooClose: "L'expérience de l'utilisateur peut être gravement compromise par un intervalle de diffusion de publicités extrêmement court."
_forgotPassword: _forgotPassword:
enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte. Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette adresse." enterEmail: "Entrez ici l'adresse e-mail que vous avez enregistrée pour votre compte. Un lien vous permettant de réinitialiser votre mot de passe sera envoyé à cette adresse."
ifNoEmail: "Si vous n'avez pas enregistré d'adresse e-mail, merci de contacter l'administrateur·rice de votre instance." ifNoEmail: "Si vous n'avez pas enregistré d'adresse e-mail, merci de contacter l'administrateur·rice de votre instance."
@ -1243,9 +1301,9 @@ _email:
_receiveFollowRequest: _receiveFollowRequest:
title: "Vous avez reçu une demande de suivi" title: "Vous avez reçu une demande de suivi"
_plugin: _plugin:
install: "Installation de plugin" install: "Installation d'extensions"
installWarn: "Ninstallez que des extensions provenant de sources de confiance." installWarn: "Ninstallez que des extensions provenant de sources de confiance."
manage: "Gestion des plugins" manage: "Gestion des extensions"
viewSource: "Afficher la source" viewSource: "Afficher la source"
_preferencesBackups: _preferencesBackups:
list: "Sauvegardes créées" list: "Sauvegardes créées"
@ -1280,6 +1338,7 @@ _aboutMisskey:
donate: "Soutenir Misskey" donate: "Soutenir Misskey"
morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰" morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰"
patrons: "Contributeurs" patrons: "Contributeurs"
projectMembers: "Membres du projet"
_displayOfSensitiveMedia: _displayOfSensitiveMedia:
force: "Masquer tous les médias" force: "Masquer tous les médias"
_instanceTicker: _instanceTicker:
@ -1575,6 +1634,7 @@ _exportOrImport:
userLists: "Listes" userLists: "Listes"
excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine" excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine"
excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs" excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs"
withReplies: "Inclure les réponses des utilisateur·rice·s importé·e·s dans le fil"
_charts: _charts:
federation: "Fédération" federation: "Fédération"
apRequest: "Requêtes" apRequest: "Requêtes"
@ -1671,7 +1731,7 @@ _notification:
youGotReply: "Réponse de {name}" youGotReply: "Réponse de {name}"
youGotQuote: "Cité·e par {name}" youGotQuote: "Cité·e par {name}"
youRenoted: "{name} vous a Renoté" youRenoted: "{name} vous a Renoté"
youWereFollowed: "Vous suit" youWereFollowed: "s'est abonné·e à vous"
youReceivedFollowRequest: "Vous avez reçu une demande dabonnement" youReceivedFollowRequest: "Vous avez reçu une demande dabonnement"
yourFollowRequestAccepted: "Votre demande dabonnement a été accepté" yourFollowRequestAccepted: "Votre demande dabonnement a été accepté"
pollEnded: "Les résultats du sondage sont disponibles" pollEnded: "Les résultats du sondage sont disponibles"
@ -1726,5 +1786,85 @@ _webhookSettings:
name: "Nom" name: "Nom"
active: "Activé" active: "Activé"
_moderationLogTypes: _moderationLogTypes:
suspend: "Suspendre" createRole: "Rôle créé"
resetPassword: "Réinitialiser le mot de passe" deleteRole: "Rôle supprimé"
updateRole: "Rôle mis à jour"
assignRole: "Rôle attribué"
unassignRole: "Rôle enlevé"
suspend: "Utilisateur suspendu"
unsuspend: "Suspension d'un utilisateur levée"
addCustomEmoji: "Émoji personnalisé ajouté"
updateCustomEmoji: "Émoji personnalisé mis à jour"
deleteCustomEmoji: "Émoji personnalisé supprimé"
updateServerSettings: "Paramètres du serveur mis à jour"
updateUserNote: "Note de modération mise à jour"
deleteDriveFile: "Fichier supprimé"
deleteNote: "Note supprimée"
createGlobalAnnouncement: "Annonce globale créée"
createUserAnnouncement: "Annonce individuelle créée"
updateGlobalAnnouncement: "Annonce globale mise à jour"
updateUserAnnouncement: "Annonce individuelle mise à jour"
deleteGlobalAnnouncement: "Annonce globale supprimée"
deleteUserAnnouncement: "Annonce individuelle supprimée"
resetPassword: "Mot de passe réinitialisé"
suspendRemoteInstance: "Instance distante suspendue"
unsuspendRemoteInstance: "Suspension d'une instance distante levée"
markSensitiveDriveFile: "Fichier marqué comme sensible"
unmarkSensitiveDriveFile: "Marquage du fichier comme sensible enlevé"
resolveAbuseReport: "Signalement résolu"
createInvitation: "Code d'invitation créé"
createAd: "Publicité créée"
deleteAd: "Publicité supprimée"
updateAd: "Publicité mise à jour"
createAvatarDecoration: "Décoration d'avatar créée"
updateAvatarDecoration: "Décoration d'avatar mise à jour"
deleteAvatarDecoration: "Décoration d'avatar supprimée"
_fileViewer:
title: "Détails du fichier"
type: "Type du fichier"
size: "Taille du fichier"
url: "URL"
uploadedAt: "Date de téléversement"
attachedNotes: "Notes avec ce fichier"
thisPageCanBeSeenFromTheAuthor: "Cette page ne peut être vue que par l'utilisateur qui a téléversé ce fichier."
_externalResourceInstaller:
title: "Installer depuis un site externe"
checkVendorBeforeInstall: "Veuillez confirmer que le distributeur est fiable avant l'installation."
_plugin:
title: "Voulez-vous installer cette extension ?"
metaTitle: "Informations sur l'extension"
_theme:
title: "Voulez-vous installer ce thème ?"
metaTitle: "Informations sur le thème"
_meta:
base: "Palette de couleurs de base"
_vendorInfo:
title: "Informations sur le distributeur"
endpoint: "Point de terminaison référencé"
hashVerify: "Vérification de l'intégrité du fichier"
_errors:
_invalidParams:
title: "Paramètres invalides"
description: "Il y a un manque d'informations nécessaires pour obtenir des données à partir de sites externes. Veuillez vérifier l'URL."
_resourceTypeNotSupported:
title: "Cette ressource externe n'est pas prise en charge."
description: "Le type de ressource obtenue à partir de ce site externe n'est pas pris en charge. Veuillez contacter l'administrateur du site."
_failedToFetch:
title: "Échec de récupération des données"
fetchErrorDescription: "La communication avec le site externe a échoué. Si vous réessayez et que cela ne s'améliore pas, veuillez contacter l'administrateur du site."
parseErrorDescription: "Les données obtenues à partir du site externe n'ont pas pu être parsées. Veuillez contacter l'administrateur du site."
_hashUnmatched:
title: "Échec de vérification des données"
description: "La vérification de l'intégrité des données fournies a échoué. Pour des raisons de sécurité, l'installation ne peut pas continuer. Veuillez contacter l'administrateur du site."
_pluginParseFailed:
title: "Erreur d'AiScript"
description: "Bien que les données aient été obtenues, elles n'ont pas pu être lues, car il y a eu une erreur lors du parsage d'AiScript. Veuillez contacter l'auteur de l'extension. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript."
_pluginInstallFailed:
title: "Échec d'installation de l'extension"
description: "Il y a eu un problème lors de l'installation de l'extension. Veuillez réessayer. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript."
_themeParseFailed:
title: "Erreur de parsage du thème"
description: "Bien que les données aient été obtenues, elles n'ont pas pu être lues, car il y a eu une erreur lors du parsage du fichier du thème. Veuillez contacter l'auteur du thème. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript."
_themeInstallFailed:
title: "Échec d'installation du thème"
description: "Il y a eu un problème lors de l'installation du thème. Veuillez réessayer. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript."

View file

@ -1 +1,5 @@
--- ---
_lang_: "japanski"
ok: "OK"
gotIt: "Razumijem"
cancel: "otkazati"

View file

@ -1 +1,18 @@
--- ---
_lang_: "Japonè"
password: "modpas"
ok: "OK"
gotIt: "Konprann"
cancel: "anile"
noThankYou: "Sispann"
instance: "sèvè"
profile: "pwofil"
save: "kenbe"
delete: "efase"
instances: "sèvè"
remove: "efase"
smtpPass: "modpas"
_2fa:
renewTOTPCancel: "Sispann"
_widgets:
profile: "pwofil"

View file

@ -45,6 +45,7 @@ pin: "Sematkan ke profil"
unpin: "Lepas sematan dari profil" unpin: "Lepas sematan dari profil"
copyContent: "Salin konten" copyContent: "Salin konten"
copyLink: "Salin tautan" copyLink: "Salin tautan"
copyLinkRenote: "Salin tautan renote"
delete: "Hapus" delete: "Hapus"
deleteAndEdit: "Hapus dan sunting" deleteAndEdit: "Hapus dan sunting"
deleteAndEditConfirm: "Apakah kamu yakin ingin menghapus note ini dan menyuntingnya? Kamu akan kehilangan semua reaksi, renote dan balasan di note ini." deleteAndEditConfirm: "Apakah kamu yakin ingin menghapus note ini dan menyuntingnya? Kamu akan kehilangan semua reaksi, renote dan balasan di note ini."
@ -156,6 +157,7 @@ addEmoji: "Tambahkan emoji"
settingGuide: "Pengaturan rekomendasi" settingGuide: "Pengaturan rekomendasi"
cacheRemoteFiles: "Tembolokkan berkas dari instansi luar" cacheRemoteFiles: "Tembolokkan berkas dari instansi luar"
cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas dari instansi luar akan dimuat langsung. Menonaktifkan ini akan mengurangi penggunaan penyimpanan peladen, namun dapat menyebabkan peningkatan lalu lintas bandwidth, karena keluku tidak dihasilkan." cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas dari instansi luar akan dimuat langsung. Menonaktifkan ini akan mengurangi penggunaan penyimpanan peladen, namun dapat menyebabkan peningkatan lalu lintas bandwidth, karena keluku tidak dihasilkan."
youCanCleanRemoteFilesCache: "Kamu dapat mengosongkan tembolok dengan mengeklik tombol 🗑️ pada layar manajemen berkas."
cacheRemoteSensitiveFiles: "Tembolokkan berkas dari instansi luar" cacheRemoteSensitiveFiles: "Tembolokkan berkas dari instansi luar"
cacheRemoteSensitiveFilesDescription: "Menonaktifkan pengaturan ini menyebabkan berkas sensitif dari instansi luar ditautkan secara langsung, bukan ditembolok." cacheRemoteSensitiveFilesDescription: "Menonaktifkan pengaturan ini menyebabkan berkas sensitif dari instansi luar ditautkan secara langsung, bukan ditembolok."
flagAsBot: "Atur akun ini sebagai Bot" flagAsBot: "Atur akun ini sebagai Bot"
@ -193,6 +195,7 @@ perHour: "per Jam"
perDay: "per Hari" perDay: "per Hari"
stopActivityDelivery: "Berhenti mengirim aktivitas" stopActivityDelivery: "Berhenti mengirim aktivitas"
blockThisInstance: "Blokir instansi ini" blockThisInstance: "Blokir instansi ini"
silenceThisInstance: "Senyapkan instansi ini"
operations: "Tindakan" operations: "Tindakan"
software: "Perangkat lunak" software: "Perangkat lunak"
version: "Versi" version: "Versi"
@ -212,6 +215,8 @@ clearCachedFiles: "Hapus tembolok"
clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas instansi luar?" clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas instansi luar?"
blockedInstances: "Instansi terblokir" blockedInstances: "Instansi terblokir"
blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini." blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini."
silencedInstances: "Instansi yang disenyapkan"
silencedInstancesDescription: "Daftar nama host dari instansi yang ingin kamu senyapkan. Semua akun dari instansi yang terdaftar akan diperlakukan sebagai disenyapkan. Hal ini membuat akun hanya dapat membuat permintaan mengikuti, dan tidak dapat menyebutkan akun lokal apabila tidak mengikuti. Hal ini tidak akan mempengaruhi instansi yang diblokir."
muteAndBlock: "Bisukan / Blokir" muteAndBlock: "Bisukan / Blokir"
mutedUsers: "Pengguna yang dibisukan" mutedUsers: "Pengguna yang dibisukan"
blockedUsers: "Pengguna yang diblokir" blockedUsers: "Pengguna yang diblokir"
@ -409,10 +414,14 @@ aboutMisskey: "Tentang Misskey"
administrator: "Admin" administrator: "Admin"
token: "Token" token: "Token"
2fa: "Autentikasi 2-faktor" 2fa: "Autentikasi 2-faktor"
setupOf2fa: "Atur autentikasi 2-faktor"
totp: "Aplikasi autentikator" totp: "Aplikasi autentikator"
totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai" totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai"
moderator: "Moderator" moderator: "Moderator"
moderation: "Moderasi" moderation: "Moderasi"
moderationNote: "Catatan moderasi"
addModerationNote: "Tambahkan catatan moderasi"
moderationLogs: "Log moderasi"
nUsersMentioned: "{n} pengguna disebut" nUsersMentioned: "{n} pengguna disebut"
securityKeyAndPasskey: "Security key dan passkey" securityKeyAndPasskey: "Security key dan passkey"
securityKey: "Kunci keamanan" securityKey: "Kunci keamanan"
@ -435,7 +444,7 @@ markAsReadAllTalkMessages: "Tandai semua pesan telah dibaca"
help: "Bantuan" help: "Bantuan"
inputMessageHere: "Ketik pesan disini" inputMessageHere: "Ketik pesan disini"
close: "Tutup" close: "Tutup"
invites: "Undang" invites: "Undangan"
members: "Anggota" members: "Anggota"
transfer: "Transfer" transfer: "Transfer"
title: "Judul" title: "Judul"
@ -450,7 +459,7 @@ noMessagesYet: "Tidak ada pesan"
newMessageExists: "Kamu mendapatkan pesan baru" newMessageExists: "Kamu mendapatkan pesan baru"
onlyOneFileCanBeAttached: "Kamu hanya dapat melampirkan satu berkas ke dalam pesan" onlyOneFileCanBeAttached: "Kamu hanya dapat melampirkan satu berkas ke dalam pesan"
signinRequired: "Silahkan login" signinRequired: "Silahkan login"
invitations: "Undang" invitations: "Undangan"
invitationCode: "Kode undangan" invitationCode: "Kode undangan"
checking: "Memeriksa" checking: "Memeriksa"
available: "Tersedia" available: "Tersedia"
@ -506,7 +515,7 @@ showFeaturedNotesInTimeline: "Tampilkan catatan yang diunggulkan di lini masa"
objectStorage: "Object Storage" objectStorage: "Object Storage"
useObjectStorage: "Gunakan object storage" useObjectStorage: "Gunakan object storage"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "Prefix URL digunakan untuk mengkonstruksi URL ke object (media) referencing. Tentukan URL jika kamu menggunakan CDN atau Proxy, jika tidak tentukan alamat yang dapat diakses secara publik sesuai dengan panduan dari layanan yang akan kamu gunakan, contohnya. 'https://<bucket>.s3.amazonaws.com' untuk AWS S3, dan 'https://storage.googleapis.com/<bucket>' untuk GCS." objectStorageBaseUrlDesc: "Prefix URL digunakan untuk mengonstruksi URL ke object (media) referencing. Tentukan URL jika kamu menggunakan CDN atau Proxy. Jika tidak, tentukan alamat yang dapat diakses secara publik sesuai dengan panduan dari layanan yang akan kamu gunakan. Contohnya: 'https://<bucket>.s3.amazonaws.com' untuk AWS S3, dan 'https://storage.googleapis.com/<bucket>' untuk GCS."
objectStorageBucket: "Bucket" objectStorageBucket: "Bucket"
objectStorageBucketDesc: "Mohon tentukan nama bucket yang digunakan pada layanan yang telah dikonfigurasi." objectStorageBucketDesc: "Mohon tentukan nama bucket yang digunakan pada layanan yang telah dikonfigurasi."
objectStoragePrefix: "Prefix" objectStoragePrefix: "Prefix"
@ -523,8 +532,9 @@ objectStorageSetPublicRead: "Setel \"public-read\" disaat mengunggah"
s3ForcePathStyleDesc: "Jika s3ForcePathStyle dinyalakan, nama bucket harus dimasukkan dalam path URL dan bukan URL nama host tersebut. Kamu perlu menyalakan pengaturan ini jika menggunakan layanan seperti instansi Minio yang self-hosted." s3ForcePathStyleDesc: "Jika s3ForcePathStyle dinyalakan, nama bucket harus dimasukkan dalam path URL dan bukan URL nama host tersebut. Kamu perlu menyalakan pengaturan ini jika menggunakan layanan seperti instansi Minio yang self-hosted."
serverLogs: "Log Peladen" serverLogs: "Log Peladen"
deleteAll: "Hapus semua" deleteAll: "Hapus semua"
showFixedPostForm: "Tampilkan form posting di atas lini masa." showFixedPostForm: "Tampilkan form posting di atas lini masa"
showFixedPostFormInChannel: "Tampilkan form posting di atas lini masa (Kanal)" showFixedPostFormInChannel: "Tampilkan form posting di atas lini masa (Kanal)"
withRepliesByDefaultForNewlyFollowed: "Termasuk balasan dari pengguna baru yang diikuti pada lini masa secara bawaan"
newNoteRecived: "Kamu mendapat catatan baru" newNoteRecived: "Kamu mendapat catatan baru"
sounds: "Bunyi" sounds: "Bunyi"
sound: "Bunyi" sound: "Bunyi"
@ -627,7 +637,7 @@ testEmail: "Tes pengiriman surel"
wordMute: "Bisukan kata" wordMute: "Bisukan kata"
regexpError: "Kesalahan ekspresi reguler" regexpError: "Kesalahan ekspresi reguler"
regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab} kata yang dibisukan:" regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab} kata yang dibisukan:"
instanceMute: "Bisuka instansi" instanceMute: "Bisukan instansi"
userSaysSomething: "{name} mengatakan sesuatu" userSaysSomething: "{name} mengatakan sesuatu"
makeActive: "Aktifkan" makeActive: "Aktifkan"
display: "Tampilkan" display: "Tampilkan"
@ -652,6 +662,7 @@ behavior: "Perilaku"
sample: "Contoh" sample: "Contoh"
abuseReports: "Laporkan" abuseReports: "Laporkan"
reportAbuse: "Laporkan" reportAbuse: "Laporkan"
reportAbuseRenote: "Laporkan renote"
reportAbuseOf: "Laporkan {name}" reportAbuseOf: "Laporkan {name}"
fillAbuseReportDescription: "Mohon isi rincian laporan. Jika laporan ini mengenai catatan yang spesifik, mohon lampirkan serta URL catatan tersebut." fillAbuseReportDescription: "Mohon isi rincian laporan. Jika laporan ini mengenai catatan yang spesifik, mohon lampirkan serta URL catatan tersebut."
abuseReported: "Laporan kamu telah dikirimkan. Terima kasih." abuseReported: "Laporan kamu telah dikirimkan. Terima kasih."
@ -704,6 +715,7 @@ lockedAccountInfo: "Kecuali kamu menyetel visibilitas catatan milikmu ke \"Hanya
alwaysMarkSensitive: "Tandai media dalam catatan sebagai media sensitif" alwaysMarkSensitive: "Tandai media dalam catatan sebagai media sensitif"
loadRawImages: "Tampilkan lampiran gambar secara penuh daripada thumbnail" loadRawImages: "Tampilkan lampiran gambar secara penuh daripada thumbnail"
disableShowingAnimatedImages: "Jangan mainkan gambar bergerak" disableShowingAnimatedImages: "Jangan mainkan gambar bergerak"
highlightSensitiveMedia: "Sorot media sensitif"
verificationEmailSent: "Surel verifikasi telah dikirimkan. Mohon akses tautan yang telah disertakan untuk menyelesaikan verifikasi." verificationEmailSent: "Surel verifikasi telah dikirimkan. Mohon akses tautan yang telah disertakan untuk menyelesaikan verifikasi."
notSet: "Tidak disetel" notSet: "Tidak disetel"
emailVerified: "Surel telah diverifikasi" emailVerified: "Surel telah diverifikasi"
@ -1018,6 +1030,7 @@ retryAllQueuesConfirmText: "Hal ini akan meningkatkan beban sementara ke peladen
enableChartsForRemoteUser: "Buat bagan data pengguna instansi luar" enableChartsForRemoteUser: "Buat bagan data pengguna instansi luar"
enableChartsForFederatedInstances: "Buat bagan data peladen instansi luar" enableChartsForFederatedInstances: "Buat bagan data peladen instansi luar"
showClipButtonInNoteFooter: "Tambahkan \"Klip\" ke menu aksi catatan" showClipButtonInNoteFooter: "Tambahkan \"Klip\" ke menu aksi catatan"
reactionsDisplaySize: "Ukuran tampilan reaksi"
noteIdOrUrl: "ID catatan atau URL" noteIdOrUrl: "ID catatan atau URL"
video: "Video" video: "Video"
videos: "Video" videos: "Video"
@ -1098,9 +1111,44 @@ icon: "Avatar"
forYou: "Untuk Anda" forYou: "Untuk Anda"
currentAnnouncements: "Pengumuman Saat Ini" currentAnnouncements: "Pengumuman Saat Ini"
pastAnnouncements: "Pengumuman Terdahulu" pastAnnouncements: "Pengumuman Terdahulu"
youHaveUnreadAnnouncements: "Terdapat pengumuman yang belum dibaca"
useSecurityKey: "Mohon ikuti instruksi peramban atau perangkat kamu untuk menggunakan kunci pengaman atau passkey."
replies: "Balasan" replies: "Balasan"
renotes: "Renote" renotes: "Renote"
loadReplies: "Tampilkan balasan"
loadConversation: "Tampilkan percakapan"
pinnedList: "Daftar yang dipin"
keepScreenOn: "Biarkan layar tetap menyala"
verifiedLink: "Tautan kepemilikan telah diverifikasi"
notifyNotes: "Beritahu mengenai catatan baru"
unnotifyNotes: "Berhenti memberitahu mengenai catatan baru"
authentication: "Autentikasi"
authenticationRequiredToContinue: "Mohon autentikasikan terlebih dahulu sebelum melanjutkan"
dateAndTime: "Tanggal dan Waktu" dateAndTime: "Tanggal dan Waktu"
showRenotes: "Tampilkan renote"
edited: "Telah disunting"
notificationRecieveConfig: "Pengaturan notifikasi"
mutualFollow: "Saling mengikuti"
fileAttachedOnly: "Hanya catatan dengan berkas"
showRepliesToOthersInTimeline: "Tampilkan balasan ke pengguna lain dalam lini masa"
hideRepliesToOthersInTimeline: "Sembunyikan balasan ke orang lain dari lini masa"
externalServices: "Layanan eksternal"
impressum: "Impressum"
impressumUrl: "Tautan Impressum"
impressumDescription: "Pada beberapa negara seperti Jerman, inklusi dari informasi kontak operator (sebuah Impressum) diperlukan secara legal untuk situs web komersil."
privacyPolicy: "Kebijakan Privasi"
privacyPolicyUrl: "Tautan Kebijakan Privasi"
tosAndPrivacyPolicy: "Syarat dan Ketentuan serta Kebijakan Privasi"
flip: "Balik"
_announcement:
forExistingUsers: "Hanya pengguna yang telah ada"
forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya."
needConfirmationToRead: "Membutuhkan konfirmasi terpisah bahwa telah dibaca"
needConfirmationToReadDescription: "Permintaan terpisah untuk mengonfirmasi menandai pengumuman ini telah dibaca akan ditampilkan apabila fitur ini dinyalakan. Pengumuman ini juga akan dikecualikan dari fungsi \"Tandai semua telah dibaca\"."
end: "Arsipkan pengumuman"
tooManyActiveAnnouncementDescription: "Terlalu banyak pengumuman dapat memperburuk pengalaman pengguna. Mohon pertimbangkan untuk mengarsipkan pengumuman yang sudah usang/tidak relevan."
readConfirmTitle: "Tandai telah dibaca?"
readConfirmText: "Aksi ini akan menandai konten dari \"{title}\" telah dibaca."
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Akun kamu telah sukses dibuat!" accountCreated: "Akun kamu telah sukses dibuat!"
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu." letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
@ -1120,6 +1168,13 @@ _serverRules:
description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan." description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan."
_serverSettings: _serverSettings:
iconUrl: "URL ikon" iconUrl: "URL ikon"
appIconDescription: "Tentukan ikon yang digunakan ketika {host} ditampilkan sebagai aplikasi."
appIconUsageExample: "Contoh: Sebagai PWA, atau ketika ditampilkan sebagai markah layar beranda pada ponsel"
appIconStyleRecommendation: "Karena ikon berkemungkinan dipotong menjadi persegi atau lingkaran, ikon dengan margin terwanai di sekeliling konten sangat direkomendasikan."
appIconResolutionMustBe: "Minimum resolusi adalah {resolution}."
manifestJsonOverride: "Ambil alih manifest.json"
shortName: "Nama pendek"
shortNameDescription: "Inisial untuk nama instansi yang dapat ditampilkan apabila nama lengkap resmi terlalu panjang."
_accountMigration: _accountMigration:
moveFrom: "Pindahkan akun lain ke akun ini" moveFrom: "Pindahkan akun lain ke akun ini"
moveFromSub: "Buat alias ke akun lain" moveFromSub: "Buat alias ke akun lain"
@ -1374,6 +1429,9 @@ _achievements:
title: "Brain Diver" title: "Brain Diver"
description: "Posting tautan mengenai Brain Diver" description: "Posting tautan mengenai Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma" flavor: "Misskey-Misskey La-Tu-Ma"
_smashTestNotificationButton:
title: "Tes overflow"
description: "Picu tes notifikasi secara berulang dalam waktu yang sangat pendek"
_role: _role:
new: "Buat peran" new: "Buat peran"
edit: "Sunting peran" edit: "Sunting peran"
@ -1431,6 +1489,7 @@ _role:
descriptionOfRateLimitFactor: "Batas kecepatan yang rendah tidak begitu membatasi, batas kecepatan tinggi lebih membatasi. " descriptionOfRateLimitFactor: "Batas kecepatan yang rendah tidak begitu membatasi, batas kecepatan tinggi lebih membatasi. "
canHideAds: "Dapat menyembunyikan iklan" canHideAds: "Dapat menyembunyikan iklan"
canSearchNotes: "Penggunaan pencarian catatan" canSearchNotes: "Penggunaan pencarian catatan"
canUseTranslator: "Penggunaan penerjemah"
_condition: _condition:
isLocal: "Pengguna lokal" isLocal: "Pengguna lokal"
isRemote: "Pengguna remote" isRemote: "Pengguna remote"
@ -1479,6 +1538,10 @@ _ad:
reduceFrequencyOfThisAd: "Tampilkan iklan ini lebih sedikit" reduceFrequencyOfThisAd: "Tampilkan iklan ini lebih sedikit"
hide: "Jangan tampilkan" hide: "Jangan tampilkan"
timezoneinfo: "Hari dalam satu minggu ditentukan dari zona waktu peladen." timezoneinfo: "Hari dalam satu minggu ditentukan dari zona waktu peladen."
adsSettings: "Pengaturan iklan"
notesPerOneAd: "Interval penempatan pemutakhiran iklan secara real-time (catatan per iklan)"
setZeroToDisable: "Atur nilai ini ke 0 untuk menonaktifkan pemutakhiran iklan secara real-time"
adsTooClose: "Interval iklan saat ini kemungkinan memperburuk pengalaman pengguna secara signifikan karena diatur pada nilai yang terlalu rendah."
_forgotPassword: _forgotPassword:
enterEmail: "Masukkan alamat surel yang kamu gunakan pada saat mendaftar. Sebuah tautan untuk mengatur ulang kata sandi kamu akan dikirimkan ke alamat surel tersebut." enterEmail: "Masukkan alamat surel yang kamu gunakan pada saat mendaftar. Sebuah tautan untuk mengatur ulang kata sandi kamu akan dikirimkan ke alamat surel tersebut."
ifNoEmail: "Apabila kamu tidak menggunakan surel pada saat pendaftaran, mohon hubungi admin segera." ifNoEmail: "Apabila kamu tidak menggunakan surel pada saat pendaftaran, mohon hubungi admin segera."
@ -1673,17 +1736,19 @@ _timelineTutorial:
step4_1: "Kamu dapat menyisipkan \"Reaksi\" ke dalam catatan." step4_1: "Kamu dapat menyisipkan \"Reaksi\" ke dalam catatan."
step4_2: "Untuk menyisipkan reaksi, tekan tanda \"+\" dalam catatan dan pilih emoji yang kamu suka untuk mereaksi catatan tersebut." step4_2: "Untuk menyisipkan reaksi, tekan tanda \"+\" dalam catatan dan pilih emoji yang kamu suka untuk mereaksi catatan tersebut."
_2fa: _2fa:
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." alreadyRegistered: "Kamu telah mendaftarkan perangkat autentikasi 2-faktor."
registerTOTP: "Daftarkan aplikasi autentikator" registerTOTP: "Daftarkan aplikasi autentikator"
step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu." step1: "Pertama, pasang aplikasi autentikasi (seperti {a} atau {b}) di perangkat kamu."
step2: "Lalu, pindai kode QR yang ada di layar." step2: "Lalu, pindai kode QR yang ada di layar."
step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel." step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel."
step2Uri: "Masukkan URI berikut jika kamu menggunakan program desktop"
step3Title: "Masukkan kode autentikasi" step3Title: "Masukkan kode autentikasi"
step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan."
step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu." setupCompleted: "Penyetelan autentikasi 2-faktor selesai"
step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi autentikasi kamu."
securityKeyNotSupported: "Peramban kamu tidak mendukung security key." securityKeyNotSupported: "Peramban kamu tidak mendukung security key."
registerTOTPBeforeKey: "Mohon atur aplikasi autentikator untuk mendaftarkan security key atau passkey." registerTOTPBeforeKey: "Mohon atur aplikasi autentikator untuk mendaftarkan security key atau passkey."
securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu." securityKeyInfo: "Kamu dapat memasang autentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau autentikasi PIN pada perangkatmu."
registerSecurityKey: "Daftarkan security key atau passkey." registerSecurityKey: "Daftarkan security key atau passkey."
securityKeyName: "Masukkan nama key." securityKeyName: "Masukkan nama key."
tapSecurityKey: "Mohon ikuti peramban kamu untuk mendaftarkan security key atau passkey" tapSecurityKey: "Mohon ikuti peramban kamu untuk mendaftarkan security key atau passkey"
@ -1694,7 +1759,11 @@ _2fa:
renewTOTPConfirm: "Hal ini akan menyebabkan kode verifikasi dari aplikasi autentikator sebelumnya berhenti bekerja" renewTOTPConfirm: "Hal ini akan menyebabkan kode verifikasi dari aplikasi autentikator sebelumnya berhenti bekerja"
renewTOTPOk: "Atur ulang" renewTOTPOk: "Atur ulang"
renewTOTPCancel: "Tidak sekarang." renewTOTPCancel: "Tidak sekarang."
checkBackupCodesBeforeCloseThisWizard: "Sebelum kamu menutup jendela ini, pastikan untuk memperhatikan dan mencadangkan kode cadangan berikut."
backupCodes: "Kode Pencadangan" backupCodes: "Kode Pencadangan"
backupCodesDescription: "Kamu dapat menggunakan kode ini untuk mendapatkan akses ke akun kamu apabila berada dalam situasi tidak dapat menggunakan aplikasi autentikasi 2-faktor yang kamu miliki. Setiap kode hanya dapat digunakan satu kali. Mohon simpan kode ini di tempat yang aman."
backupCodeUsedWarning: "Kode cadangan telah digunakan. Mohon mengatur ulang autentikasi 2-faktor secepatnya apabila kamu sudah tidak dapat menggunakannya lagi."
backupCodesExhaustedWarning: "Semua kode cadangan telah digunakan. Apabila kamu kehilangan akses pada aplikasi autentikasi 2-faktor milikmu, kamu tidak dapat mengakses akun ini lagi. Mohon atur ulang autentikasi 2-faktor kamu."
_permissions: _permissions:
"read:account": "Lihat informasi akun" "read:account": "Lihat informasi akun"
"write:account": "Sunting informasi akun" "write:account": "Sunting informasi akun"
@ -1728,6 +1797,10 @@ _permissions:
"write:gallery": "Sunting galeri" "write:gallery": "Sunting galeri"
"read:gallery-likes": "Lihat daftar postingan galeri yang disukai" "read:gallery-likes": "Lihat daftar postingan galeri yang disukai"
"write:gallery-likes": "Sunting daftar postingan galeri yang disukai" "write:gallery-likes": "Sunting daftar postingan galeri yang disukai"
"read:flash": "Lihat Play"
"write:flash": "Sunting Play"
"read:flash-likes": "Lihat daftar Play yang disukai"
"write:flash-likes": "Sunting daftar Play yang disukai"
_auth: _auth:
shareAccessTitle: "Mendapatkan ijin akses aplikasi" shareAccessTitle: "Mendapatkan ijin akses aplikasi"
shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?" shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?"
@ -1743,6 +1816,7 @@ _antennaSources:
homeTimeline: "Catatan dari pengguna yang diikuti" homeTimeline: "Catatan dari pengguna yang diikuti"
users: "Catatan dari pengguna tertentu" users: "Catatan dari pengguna tertentu"
userList: "Catatan dari daftar tertentu" userList: "Catatan dari daftar tertentu"
userBlacklist: "Semua catatan kecuali untuk satu pengguna atau lebih yang telah ditentukan"
_weekday: _weekday:
sunday: "Minggu" sunday: "Minggu"
monday: "Senin" monday: "Senin"
@ -1842,6 +1916,7 @@ _profile:
metadataContent: "Isi" metadataContent: "Isi"
changeAvatar: "Ubah avatar" changeAvatar: "Ubah avatar"
changeBanner: "Ubah header" changeBanner: "Ubah header"
verifiedLinkDescription: "Dengan memasukkan URL yang mengandung tautan ke profil kamu di sini, ikon verifikasi kepemilikan dapat ditampilkan di sebelah kolom ini."
_exportOrImport: _exportOrImport:
allNotes: "Semua catatan" allNotes: "Semua catatan"
favoritedNotes: "Catatan favorit" favoritedNotes: "Catatan favorit"
@ -1851,6 +1926,7 @@ _exportOrImport:
userLists: "Daftar" userLists: "Daftar"
excludeMutingUsers: "Kecualikan pengguna yang dibisukan" excludeMutingUsers: "Kecualikan pengguna yang dibisukan"
excludeInactiveUsers: "Kecualikan pengguna tidak aktif" excludeInactiveUsers: "Kecualikan pengguna tidak aktif"
withReplies: "Termasuk balasan dari pengguna yang diimpor ke dalam lini masa"
_charts: _charts:
federation: "Federasi" federation: "Federasi"
apRequest: "Permintaan" apRequest: "Permintaan"
@ -1960,11 +2036,17 @@ _notification:
youReceivedFollowRequest: "Kamu menerima permintaan mengikuti" youReceivedFollowRequest: "Kamu menerima permintaan mengikuti"
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
pollEnded: "Hasil Kuesioner telah keluar" pollEnded: "Hasil Kuesioner telah keluar"
newNote: "Catatan baru"
unreadAntennaNote: "Antena {name}" unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Pembaruan notifikasi dorong" emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
achievementEarned: "Pencapaian didapatkan" achievementEarned: "Pencapaian didapatkan"
testNotification: "Tes notifikasi"
checkNotificationBehavior: "Cek tampilan notifikasi"
sendTestNotification: "Kirim tes notifikasi"
notificationWillBeDisplayedLikeThis: "Notifikasi akan terlihat seperti ini"
_types: _types:
all: "Semua" all: "Semua"
note: "Catatan baru"
follow: "Ikuti" follow: "Ikuti"
mention: "Sebut" mention: "Sebut"
reply: "Balasan" reply: "Balasan"
@ -1998,6 +2080,8 @@ _deck:
introduction2: "Klik \"+\" pada kanan layar untuk menambahkan kolom baru kapanpun yang kamu mau." introduction2: "Klik \"+\" pada kanan layar untuk menambahkan kolom baru kapanpun yang kamu mau."
widgetsIntroduction: "Mohon pilih \"Sunting gawit\" pada menu kolom dan tambahkan gawit." widgetsIntroduction: "Mohon pilih \"Sunting gawit\" pada menu kolom dan tambahkan gawit."
useSimpleUiForNonRootPages: "Gunakan antarmuka sederhana ke halaman yang dituju" useSimpleUiForNonRootPages: "Gunakan antarmuka sederhana ke halaman yang dituju"
usedAsMinWidthWhenFlexible: "Lebar minimum akan digunakan untuk ini ketika opsi \"Atur-otomatis lebar\" dinyalakan"
flexible: "Atur-otomatis lebar"
_columns: _columns:
main: "Utama" main: "Utama"
widgets: "Widget" widgets: "Widget"
@ -2033,6 +2117,41 @@ _webhookSettings:
reaction: "Ketika menerima reaksi" reaction: "Ketika menerima reaksi"
mention: "Ketika sedang disebut" mention: "Ketika sedang disebut"
_moderationLogTypes: _moderationLogTypes:
createRole: "Peran telah dibuat"
deleteRole: "Peran telah dihapus"
updateRole: "Peran telah diperbaharui"
assignRole: "Yang ditugaskan dalam peran"
unassignRole: "Dihapus dari peran"
suspend: "Tangguhkan" suspend: "Tangguhkan"
unsuspend: "Batal ditangguhkan"
addCustomEmoji: "Emoji kustom ditambahkan"
updateCustomEmoji: "Emoji kustom diperbaharui"
deleteCustomEmoji: "Emoji kustom dihapus"
updateServerSettings: "Pengaturan peladen diperbaharui"
updateUserNote: "Catatan moderasi diperbaharui"
deleteDriveFile: "Berkas dihapus"
deleteNote: "Catatan dihapus"
createGlobalAnnouncement: "Pengumuman global dibuat"
createUserAnnouncement: "Pengumuman pengguna dibuat"
updateGlobalAnnouncement: "Pengumuman global diperbaharui"
updateUserAnnouncement: "Pengumuman pengguna diperbaharui"
deleteGlobalAnnouncement: "Pengumuman global telah dihapus"
deleteUserAnnouncement: "Pengumuman pengguna telah dihapus."
resetPassword: "Atur ulang kata sandi" resetPassword: "Atur ulang kata sandi"
suspendRemoteInstance: "Instansi luar telah ditangguhkan"
unsuspendRemoteInstance: "Instansi luar batal ditangguhkan"
markSensitiveDriveFile: "Berkas ditandai sensitif"
unmarkSensitiveDriveFile: "Berkas batal ditandai sensitif"
resolveAbuseReport: "Laporan terselesaikan"
createInvitation: "Buat kode undangan" createInvitation: "Buat kode undangan"
createAd: "Iklan telah dibuat"
deleteAd: "Iklan telah dihapus"
updateAd: "Iklan telah diperbaharui"
_fileViewer:
title: "Rincian berkas"
type: "Jenis berkas"
size: "Ukuran berkas"
url: "URL"
uploadedAt: "Diunggah pada"
attachedNotes: "Catatan yang dilampirkan"
thisPageCanBeSeenFromTheAuthor: "Halaman ini hanya dapat dilihat oleh pengguna yang mengunggah bekas ini."

76
locales/index.d.ts vendored
View file

@ -1002,6 +1002,7 @@ export interface Locale {
"unassign": string; "unassign": string;
"color": string; "color": string;
"manageCustomEmojis": string; "manageCustomEmojis": string;
"manageAvatarDecorations": string;
"youCannotCreateAnymore": string; "youCannotCreateAnymore": string;
"cannotPerformTemporary": string; "cannotPerformTemporary": string;
"cannotPerformTemporaryDescription": string; "cannotPerformTemporaryDescription": string;
@ -1164,6 +1165,10 @@ export interface Locale {
"fileAttachedOnly": string; "fileAttachedOnly": string;
"showRepliesToOthersInTimeline": string; "showRepliesToOthersInTimeline": string;
"hideRepliesToOthersInTimeline": string; "hideRepliesToOthersInTimeline": string;
"showRepliesToOthersInTimelineAll": string;
"hideRepliesToOthersInTimelineAll": string;
"confirmShowRepliesAll": string;
"confirmHideRepliesAll": string;
"externalServices": string; "externalServices": string;
"impressum": string; "impressum": string;
"impressumUrl": string; "impressumUrl": string;
@ -1171,6 +1176,16 @@ export interface Locale {
"privacyPolicy": string; "privacyPolicy": string;
"privacyPolicyUrl": string; "privacyPolicyUrl": string;
"tosAndPrivacyPolicy": string; "tosAndPrivacyPolicy": string;
"avatarDecorations": string;
"attach": string;
"detach": string;
"angle": string;
"flip": string;
"showAvatarDecorations": string;
"releaseToRefresh": string;
"refreshing": string;
"pullDownToRefresh": string;
"disableStreamingTimeline": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;
@ -1209,6 +1224,7 @@ export interface Locale {
"manifestJsonOverride": string; "manifestJsonOverride": string;
"shortName": string; "shortName": string;
"shortNameDescription": string; "shortNameDescription": string;
"fanoutTimelineDescription": string;
}; };
"_accountMigration": { "_accountMigration": {
"moveFrom": string; "moveFrom": string;
@ -1589,6 +1605,7 @@ export interface Locale {
"inviteLimitCycle": string; "inviteLimitCycle": string;
"inviteExpirationTime": string; "inviteExpirationTime": string;
"canManageCustomEmojis": string; "canManageCustomEmojis": string;
"canManageAvatarDecorations": string;
"driveCapacity": string; "driveCapacity": string;
"alwaysMarkNsfw": string; "alwaysMarkNsfw": string;
"pinMax": string; "pinMax": string;
@ -1727,6 +1744,7 @@ export interface Locale {
"donate": string; "donate": string;
"morePatrons": string; "morePatrons": string;
"patrons": string; "patrons": string;
"projectMembers": string;
}; };
"_displayOfSensitiveMedia": { "_displayOfSensitiveMedia": {
"respect": string; "respect": string;
@ -2328,6 +2346,9 @@ export interface Locale {
"createAd": string; "createAd": string;
"deleteAd": string; "deleteAd": string;
"updateAd": string; "updateAd": string;
"createAvatarDecoration": string;
"updateAvatarDecoration": string;
"deleteAvatarDecoration": string;
}; };
"_fileViewer": { "_fileViewer": {
"title": string; "title": string;
@ -2338,6 +2359,61 @@ export interface Locale {
"attachedNotes": string; "attachedNotes": string;
"thisPageCanBeSeenFromTheAuthor": string; "thisPageCanBeSeenFromTheAuthor": string;
}; };
"_externalResourceInstaller": {
"title": string;
"checkVendorBeforeInstall": string;
"_plugin": {
"title": string;
"metaTitle": string;
};
"_theme": {
"title": string;
"metaTitle": string;
};
"_meta": {
"base": string;
};
"_vendorInfo": {
"title": string;
"endpoint": string;
"hashVerify": string;
};
"_errors": {
"_invalidParams": {
"title": string;
"description": string;
};
"_resourceTypeNotSupported": {
"title": string;
"description": string;
};
"_failedToFetch": {
"title": string;
"fetchErrorDescription": string;
"parseErrorDescription": string;
};
"_hashUnmatched": {
"title": string;
"description": string;
};
"_pluginParseFailed": {
"title": string;
"description": string;
};
"_pluginInstallFailed": {
"title": string;
"description": string;
};
"_themeParseFailed": {
"title": string;
"description": string;
};
"_themeInstallFailed": {
"title": string;
"description": string;
};
};
};
} }
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;

View file

@ -110,7 +110,7 @@ unrenote: "Elimina la Rinota"
renoted: "Rinotato!" renoted: "Rinotato!"
cantRenote: "È impossibile rinotare questa nota." cantRenote: "È impossibile rinotare questa nota."
cantReRenote: "È impossibile rinotare una Rinota." cantReRenote: "È impossibile rinotare una Rinota."
quote: "Cita" quote: "Citazione"
inChannelRenote: "Rinota nel canale" inChannelRenote: "Rinota nel canale"
inChannelQuote: "Cita nel canale" inChannelQuote: "Cita nel canale"
pinnedNote: "Nota in primo piano" pinnedNote: "Nota in primo piano"
@ -162,8 +162,8 @@ cacheRemoteSensitiveFiles: "Copia nella cache locale i file espliciti remoti"
cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file espliciti verranno richiesti direttamente all'istanza remota senza essere salvati nel server locale." cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file espliciti verranno richiesti direttamente all'istanza remota senza essere salvati nel server locale."
flagAsBot: "Io sono un robot" flagAsBot: "Io sono un robot"
flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot." flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot."
flagAsCat: "Sono un gatto" flagAsCat: "MIIaaaoo!!! (Io sono un gatto è un romanzo del 1905, il primo dello scrittore giapponese Natsume Sōseki)"
flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo" flagAsCatDescription: "Miaoo mia miao mi miao?"
flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline." flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline."
flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note" flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note"
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da profili che già segui" autoAcceptFollowed: "Accetta automaticamente le richieste di follow da profili che già segui"
@ -250,7 +250,7 @@ newPassword: "Nuova Password"
newPasswordRetype: "Conferma password" newPasswordRetype: "Conferma password"
attachFile: "Allega file" attachFile: "Allega file"
more: "Di più!" more: "Di più!"
featured: "Tendenze" featured: "In evidenza"
usernameOrUserId: "Nome utente o ID" usernameOrUserId: "Nome utente o ID"
noSuchUser: "Profilo non trovato" noSuchUser: "Profilo non trovato"
lookup: "Ricerca remota" lookup: "Ricerca remota"
@ -534,6 +534,7 @@ serverLogs: "Log del server"
deleteAll: "Cancella cronologia" deleteAll: "Cancella cronologia"
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline" showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline"
withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita"
newNoteRecived: "Nuove note da leggere" newNoteRecived: "Nuove note da leggere"
sounds: "Impostazioni suoni" sounds: "Impostazioni suoni"
sound: "Suono" sound: "Suono"
@ -978,6 +979,7 @@ assign: "Assegna"
unassign: "Disassegna" unassign: "Disassegna"
color: "Colore" color: "Colore"
manageCustomEmojis: "Gestisci le emoji personalizzate" manageCustomEmojis: "Gestisci le emoji personalizzate"
manageAvatarDecorations: "Gestire le decorazioni di foto del profilo"
youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite." youCannotCreateAnymore: "Non puoi creare, hai raggiunto il limite."
cannotPerformTemporary: "Indisponibilità temporanea" cannotPerformTemporary: "Indisponibilità temporanea"
cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi." cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi."
@ -1131,13 +1133,27 @@ mutualFollow: "Follow reciproco"
fileAttachedOnly: "Solo con allegati" fileAttachedOnly: "Solo con allegati"
showRepliesToOthersInTimeline: "Risposte altrui nella TL" showRepliesToOthersInTimeline: "Risposte altrui nella TL"
hideRepliesToOthersInTimeline: "Nascondi Riposte altrui nella TL" hideRepliesToOthersInTimeline: "Nascondi Riposte altrui nella TL"
showRepliesToOthersInTimelineAll: "Mostra le risposte dei tuoi follow nella TL"
hideRepliesToOthersInTimelineAll: "Nascondi le risposte dei tuoi follow nella TL"
confirmShowRepliesAll: "Questa è una attività irreversibile. Vuoi davvero includere tutte le risposte dei following in TL?"
confirmHideRepliesAll: "Questa è una attività irreversibile. Vuoi davvero escludere tutte le risposte dei following in TL?"
externalServices: "Servizi esterni" externalServices: "Servizi esterni"
impressum: "Dichiarazione di proprietà" impressum: "Dichiarazione di proprietà"
impressumUrl: "URL della dichiarazione di proprietà" impressumUrl: "URL della dichiarazione di proprietà"
impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)." impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)."
privacyPolicy: "Informativa ai sensi degli artt. 13 e 14 del Regolamento UE 2016/679 per la protezione dei dati personali (GDPR)" privacyPolicy: "Informativa ai sensi del Reg. UE 2016/679 (GDPR)"
privacyPolicyUrl: "URL della informativa privacy" privacyPolicyUrl: "URL della informativa privacy"
tosAndPrivacyPolicy: "Condizioni d'uso e informativa privacy" tosAndPrivacyPolicy: "Condizioni d'uso e informativa privacy"
avatarDecorations: "Decorazioni foto profilo"
attach: "Applica"
detach: "Rimuovi"
angle: "Angolo"
flip: "Inverti"
showAvatarDecorations: "Mostra decorazione della foto profilo"
releaseToRefresh: "Rilascia per aggiornare"
refreshing: "Aggiornamento..."
pullDownToRefresh: "Trascina per aggiornare"
disableStreamingTimeline: "Disabilitare gli aggiornamenti della TL in tempo reale"
_announcement: _announcement:
forExistingUsers: "Solo ai profili attuali" forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
@ -1173,6 +1189,7 @@ _serverSettings:
manifestJsonOverride: "Sostituire il file manifest.json" manifestJsonOverride: "Sostituire il file manifest.json"
shortName: "Abbreviazione" shortName: "Abbreviazione"
shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server." shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server."
fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare."
_accountMigration: _accountMigration:
moveFrom: "Migra un altro profilo dentro a questo" moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto" moveFromSub: "Crea un alias verso un altro profilo remoto"
@ -1473,6 +1490,7 @@ _role:
inviteLimitCycle: "Intervallo di emissione del codice di invito" inviteLimitCycle: "Intervallo di emissione del codice di invito"
inviteExpirationTime: "Scadenza del codice di invito" inviteExpirationTime: "Scadenza del codice di invito"
canManageCustomEmojis: "Gestire le emoji personalizzate" canManageCustomEmojis: "Gestire le emoji personalizzate"
canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo"
driveCapacity: "Capienza del Drive" driveCapacity: "Capienza del Drive"
alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)" alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
pinMax: "Quantità massima di Note in primo piano" pinMax: "Quantità massima di Note in primo piano"
@ -1592,6 +1610,7 @@ _aboutMisskey:
donate: "Sostieni Misskey" donate: "Sostieni Misskey"
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
patrons: "Sostenitori" patrons: "Sostenitori"
projectMembers: "Partecipanti al progetto"
_displayOfSensitiveMedia: _displayOfSensitiveMedia:
respect: "Nascondere i media espliciti" respect: "Nascondere i media espliciti"
ignore: "Non nascondere i media espliciti" ignore: "Non nascondere i media espliciti"
@ -1609,7 +1628,7 @@ _channel:
edit: "Gerisci canale" edit: "Gerisci canale"
setBanner: "Scegli intestazione" setBanner: "Scegli intestazione"
removeBanner: "Rimuovi intestazione" removeBanner: "Rimuovi intestazione"
featured: "Tendenze" featured: "Di tendenza"
owned: "I miei canali" owned: "I miei canali"
following: "Seguiti" following: "Seguiti"
usersCount: "{n} partecipanti" usersCount: "{n} partecipanti"
@ -1830,7 +1849,7 @@ _widgets:
notifications: "Notifiche" notifications: "Notifiche"
timeline: "Timeline" timeline: "Timeline"
calendar: "Calendario" calendar: "Calendario"
trends: "Tendenze" trends: "Di tendenza"
clock: "Orologio" clock: "Orologio"
rss: "Aggregatore rss" rss: "Aggregatore rss"
rssTicker: "Ticker RSS" rssTicker: "Ticker RSS"
@ -1839,7 +1858,7 @@ _widgets:
digitalClock: "Orologio digitale" digitalClock: "Orologio digitale"
unixClock: "Orologio UNIX" unixClock: "Orologio UNIX"
federation: "Federazione" federation: "Federazione"
instanceCloud: "Istanza Cloud" instanceCloud: "Nuvola di federazione"
postForm: "Finestra di pubblicazione" postForm: "Finestra di pubblicazione"
slideshow: "Diapositive" slideshow: "Diapositive"
button: "Pulsante" button: "Pulsante"
@ -1924,6 +1943,7 @@ _exportOrImport:
userLists: "Liste" userLists: "Liste"
excludeMutingUsers: "Escludere gli utenti silenziati" excludeMutingUsers: "Escludere gli utenti silenziati"
excludeInactiveUsers: "Escludere i profili inutilizzati" excludeInactiveUsers: "Escludere i profili inutilizzati"
withReplies: "Includere le risposte da profili importati nella Timeline"
_charts: _charts:
federation: "Federazione" federation: "Federazione"
apRequest: "Richieste" apRequest: "Richieste"
@ -2088,7 +2108,7 @@ _deck:
list: "Liste" list: "Liste"
channel: "Canale" channel: "Canale"
mentions: "Menzioni" mentions: "Menzioni"
direct: "Diretta" direct: "Note Dirette"
roleTimeline: "Timeline Ruolo" roleTimeline: "Timeline Ruolo"
_dialog: _dialog:
charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})" charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})"
@ -2144,6 +2164,9 @@ _moderationLogTypes:
createAd: "Banner creato" createAd: "Banner creato"
deleteAd: "Banner eliminato" deleteAd: "Banner eliminato"
updateAd: "Banner aggiornato" updateAd: "Banner aggiornato"
createAvatarDecoration: "Creazione decorazione della foto profilo"
updateAvatarDecoration: "Aggiornamento decorazione foto profilo"
deleteAvatarDecoration: "Eliminazione decorazione della foto profilo"
_fileViewer: _fileViewer:
title: "Dettagli del file" title: "Dettagli del file"
type: "Tipo di file" type: "Tipo di file"
@ -2152,3 +2175,44 @@ _fileViewer:
uploadedAt: "Caricato il" uploadedAt: "Caricato il"
attachedNotes: "Note a cui è allegato" attachedNotes: "Note a cui è allegato"
thisPageCanBeSeenFromTheAuthor: "Questa pagina può essere vista solo da chi ha caricato il file." thisPageCanBeSeenFromTheAuthor: "Questa pagina può essere vista solo da chi ha caricato il file."
_externalResourceInstaller:
title: "Installa da sito esterno"
checkVendorBeforeInstall: "Prima di installare, assicurati che la fonte sia affidabile."
_plugin:
title: "Vuoi davvero installare questo componente aggiuntivo?"
metaTitle: "Informazioni sul componente aggiuntivo"
_theme:
title: "Vuoi davvero installare questa variazione grafica?"
metaTitle: "Informazioni sulla variazione grafica"
_meta:
base: "Combinazione base di colori"
_vendorInfo:
title: "Informazioni sulla fonte"
endpoint: "Punto di riferimento della fonte"
hashVerify: "Codice di verifica della fonte"
_errors:
_invalidParams:
title: "Parametri non validi"
description: "Mancano alcuni parametri per il caricamento, per favore, verifica la URL."
_resourceTypeNotSupported:
title: "Questa risorsa esterna non è supportata"
description: "Il tipo di risorsa ottenuta da questo sito esterno non è supportato. Si prega di contattare la fonte di distribuizone."
_failedToFetch:
title: "Impossibile ottenere i dati"
fetchErrorDescription: "Si è verificato un errore di comunicazione con la fonte. Se riprovare di nuovo non aiuta, contattare la fonte di distribuzione."
parseErrorDescription: "Si è verificato un errore elaborando i dati ottenuti dalla fonte. Per favore contattare il distributore."
_hashUnmatched:
title: "Dati non verificabili, diversi da quelli della fonte"
description: "Si è verificato un errore durante la verifica di integrità dei dati ottenuti. Per sicurezza, l'installazione è stata interrotta. Contattare la fonte di distribuzione."
_pluginParseFailed:
title: "Errore AiScript"
description: "Sebbene i dati ottenuti siano validi, non è stato possibile interpretarli, perché si è verificato un errore durante l'analisi di AiScript. Si prega di contattare gli autori del componente aggiuntivo. Potresti controllare la console di Javascript per ottenere dettagli aggiuntivi."
_pluginInstallFailed:
title: "Impossibile installare il componente aggiuntivo"
description: "Si è verificato un impedimento durante l'installazione del componente aggiuntivo. Per favore riprova e consulta la console di Javascript per ottenere dettagli aggiuntivi."
_themeParseFailed:
title: "Impossibile interpretare la variazione grafica"
description: "Sebbene i dati siano stati ottenuti, non è stato possibile interpretarli, si è verificato un errore durante l'analisi della variazione grafica. Si prega di contattare gli autori. Potresti anche controllare la console di Javascript per ottenere dettagli aggiuntivi."
_themeInstallFailed:
title: "Impossibile installare la variazione grafica"
description: "Si è verificato un impedimento durante l'installazione della variazione grafica. Per favore riprova e consulta la console di Javascript per ottenere dettagli aggiuntivi."

View file

@ -999,6 +999,7 @@ assign: "アサイン"
unassign: "アサインを解除" unassign: "アサインを解除"
color: "色" color: "色"
manageCustomEmojis: "カスタム絵文字の管理" manageCustomEmojis: "カスタム絵文字の管理"
manageAvatarDecorations: "アバターデコレーションの管理"
youCannotCreateAnymore: "これ以上作成することはできません。" youCannotCreateAnymore: "これ以上作成することはできません。"
cannotPerformTemporary: "一時的に利用できません" cannotPerformTemporary: "一時的に利用できません"
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。" cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
@ -1161,6 +1162,10 @@ mutualFollow: "相互フォロー"
fileAttachedOnly: "ファイル付きのみ" fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "TLに他の人への返信を含める" showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
showRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めるようにする"
hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めないようにする"
confirmShowRepliesAll: "この操作は元の戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか"
confirmHideRepliesAll: "この操作は元の戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか"
externalServices: "外部サービス" externalServices: "外部サービス"
impressum: "運営者情報" impressum: "運営者情報"
impressumUrl: "運営者情報URL" impressumUrl: "運営者情報URL"
@ -1168,6 +1173,16 @@ impressumDescription: "ドイツなどの一部の国と地域では表示が義
privacyPolicy: "プライバシーポリシー" privacyPolicy: "プライバシーポリシー"
privacyPolicyUrl: "プライバシーポリシーURL" privacyPolicyUrl: "プライバシーポリシーURL"
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
avatarDecorations: "アイコンデコレーション"
attach: "付ける"
detach: "外す"
angle: "角度"
flip: "反転"
showAvatarDecorations: "アイコンのデコレーションを表示"
releaseToRefresh: "離してリロード"
refreshing: "リロード中"
pullDownToRefresh: "引っ張ってリロード"
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
@ -1207,6 +1222,7 @@ _serverSettings:
manifestJsonOverride: "manifest.jsonのオーバーライド" manifestJsonOverride: "manifest.jsonのオーバーライド"
shortName: "略称" shortName: "略称"
shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。" shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。"
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
_accountMigration: _accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行" moveFrom: "別のアカウントからこのアカウントに移行"
@ -1510,6 +1526,7 @@ _role:
inviteLimitCycle: "招待コードの発行間隔" inviteLimitCycle: "招待コードの発行間隔"
inviteExpirationTime: "招待コードの有効期限" inviteExpirationTime: "招待コードの有効期限"
canManageCustomEmojis: "カスタム絵文字の管理" canManageCustomEmojis: "カスタム絵文字の管理"
canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与" alwaysMarkNsfw: "ファイルにNSFWを常に付与"
pinMax: "ノートのピン留めの最大数" pinMax: "ノートのピン留めの最大数"
@ -1644,6 +1661,7 @@ _aboutMisskey:
donate: "Sharkeyに寄付" donate: "Sharkeyに寄付"
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
patrons: "支援者" patrons: "支援者"
projectMembers: "プロジェクトメンバー"
_displayOfSensitiveMedia: _displayOfSensitiveMedia:
respect: "センシティブ設定されたメディアを隠す" respect: "センシティブ設定されたメディアを隠す"
@ -2241,6 +2259,9 @@ _moderationLogTypes:
createAd: "広告を作成" createAd: "広告を作成"
deleteAd: "広告を削除" deleteAd: "広告を削除"
updateAd: "広告を更新" updateAd: "広告を更新"
createAvatarDecoration: "アイコンデコレーションを作成"
updateAvatarDecoration: "アイコンデコレーションを更新"
deleteAvatarDecoration: "アイコンデコレーションを削除"
_fileViewer: _fileViewer:
title: "ファイルの詳細" title: "ファイルの詳細"
@ -2250,3 +2271,45 @@ _fileViewer:
uploadedAt: "追加日" uploadedAt: "追加日"
attachedNotes: "添付されているノート" attachedNotes: "添付されているノート"
thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。" thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。"
_externalResourceInstaller:
title: "外部サイトからインストール"
checkVendorBeforeInstall: "配布元が信頼できるかを確認した上でインストールしてください。"
_plugin:
title: "このプラグインをインストールしますか?"
metaTitle: "プラグイン情報"
_theme:
title: "このテーマをインストールしますか?"
metaTitle: "テーマ情報"
_meta:
base: "基本のカラースキーム"
_vendorInfo:
title: "配布元情報"
endpoint: "参照したエンドポイント"
hashVerify: "ファイル整合性の確認"
_errors:
_invalidParams:
title: "パラメータが不足しています"
description: "外部サイトからデータを取得するために必要な情報が不足しています。URLをお確かめください。"
_resourceTypeNotSupported:
title: "この外部リソースには対応していません"
description: "この外部サイトから取得したリソースの種別には対応していません。サイト管理者にお問い合わせください。"
_failedToFetch:
title: "データの取得に失敗しました"
fetchErrorDescription: "外部サイトとの通信に失敗しました。もう一度試しても改善しない場合、サイト管理者にお問い合わせください。"
parseErrorDescription: "外部サイトから取得したデータが読み取れませんでした。サイト管理者にお問い合わせください。"
_hashUnmatched:
title: "正しいデータが取得できませんでした"
description: "提供されたデータの整合性の確認に失敗しました。セキュリティ上、インストールは続行できません。サイト管理者にお問い合わせください。"
_pluginParseFailed:
title: "AiScript エラー"
description: "データは取得できたものの、AiScriptの解析時にエラーがあったため読み込めませんでした。プラグインの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。"
_pluginInstallFailed:
title: "プラグインのインストールに失敗しました"
description: "プラグインのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。"
_themeParseFailed:
title: "テーマ解析エラー"
description: "データは取得できたものの、テーマファイルの解析時にエラーがあったため読み込めませんでした。テーマの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。"
_themeInstallFailed:
title: "テーマのインストールに失敗しました"
description: "テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。"

View file

@ -45,6 +45,7 @@ pin: "ピン留めしとく"
unpin: "やっぱピン留めせん" unpin: "やっぱピン留めせん"
copyContent: "内容をコピー" copyContent: "内容をコピー"
copyLink: "リンクをコピー" copyLink: "リンクをコピー"
copyLinkRenote: "リノートのリンクをコピーするで?"
delete: "ほかす" delete: "ほかす"
deleteAndEdit: "ほかして直す" deleteAndEdit: "ほかして直す"
deleteAndEditConfirm: "このートをほかしてもっかい直すこのートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん" deleteAndEditConfirm: "このートをほかしてもっかい直すこのートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん"
@ -196,6 +197,7 @@ perHour: "1時間ごと"
perDay: "1日ごと" perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送をやめる" stopActivityDelivery: "アクティビティの配送をやめる"
blockThisInstance: "このサーバーをブロックすんで" blockThisInstance: "このサーバーをブロックすんで"
silenceThisInstance: "サーバーサイレンスすんで?"
operations: "操作" operations: "操作"
software: "ソフトウェア" software: "ソフトウェア"
version: "バージョン" version: "バージョン"
@ -215,6 +217,8 @@ clearCachedFiles: "キャッシュをほかす"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?" clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "ブロックしたサーバー" blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
silencedInstances: "サーバーサイレンスされてんねん"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー" mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"
@ -412,12 +416,14 @@ aboutMisskey: "Sharkeyってなんや"
administrator: "管理者" administrator: "管理者"
token: "トークン" token: "トークン"
2fa: "二要素認証" 2fa: "二要素認証"
setupOf2fa: "二要素認証のセットアップ"
totp: "認証アプリ" totp: "認証アプリ"
totpDescription: "認証アプリ使うてワンタイムパスワードを入れる" totpDescription: "認証アプリ使うてワンタイムパスワードを入れる"
moderator: "モデレーター" moderator: "モデレーター"
moderation: "モデレーション" moderation: "モデレーション"
moderationNote: "モデレーションノート" moderationNote: "モデレーションノート"
addModerationNote: "モデレーションノートを追加するで" addModerationNote: "モデレーションノートを追加するで"
moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿" nUsersMentioned: "{n}人が投稿"
securityKeyAndPasskey: "セキュリティキー・パスキー" securityKeyAndPasskey: "セキュリティキー・パスキー"
securityKey: "セキュリティキー" securityKey: "セキュリティキー"
@ -530,6 +536,7 @@ serverLogs: "サーバーログ"
deleteAll: "全部ほかす" deleteAll: "全部ほかす"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?" showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)" showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)"
withRepliesByDefaultForNewlyFollowed: "フォローする時、デフォルトで返信をタイムラインに含むようにしよか"
newNoteRecived: "新しいノートがあるで" newNoteRecived: "新しいノートがあるで"
sounds: "サウンド" sounds: "サウンド"
sound: "サウンド" sound: "サウンド"
@ -588,7 +595,7 @@ poll: "アンケート"
useCw: "内容を隠す" useCw: "内容を隠す"
enablePlayer: "プレイヤーを開く" enablePlayer: "プレイヤーを開く"
disablePlayer: "プレイヤーを閉じる" disablePlayer: "プレイヤーを閉じる"
expandTweet: "ツイートを展開する" expandTweet: "ポストを展開する"
themeEditor: "テーマエディター" themeEditor: "テーマエディター"
description: "説明" description: "説明"
describeFile: "キャプションを付ける" describeFile: "キャプションを付ける"
@ -657,6 +664,7 @@ behavior: "動作"
sample: "サンプル" sample: "サンプル"
abuseReports: "通報" abuseReports: "通報"
reportAbuse: "通報" reportAbuse: "通報"
reportAbuseRenote: "リノート苦情だすで?"
reportAbuseOf: "{name}を通報する" reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "細かい通報理由を書いてなー。対象ートがある時はそのURLも書いといてなー。" fillAbuseReportDescription: "細かい通報理由を書いてなー。対象ートがある時はそのURLも書いといてなー。"
abuseReported: "無事内容が送信されたみたいやで。おおきに〜。" abuseReported: "無事内容が送信されたみたいやで。おおきに〜。"
@ -709,6 +717,7 @@ lockedAccountInfo: "フォローを承認制にしとっても、ノートの公
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで" alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで"
loadRawImages: "添付画像のサムネイルをオリジナル画質にするで" loadRawImages: "添付画像のサムネイルをオリジナル画質にするで"
disableShowingAnimatedImages: "アニメーション画像を再生せんとくで" disableShowingAnimatedImages: "アニメーション画像を再生せんとくで"
highlightSensitiveMedia: "メディアがセンシティブなことをめっっちゃわかりやすく表紙"
verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。" verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。"
notSet: "未設定" notSet: "未設定"
emailVerified: "メールアドレスは確認されたで" emailVerified: "メールアドレスは確認されたで"
@ -1023,6 +1032,7 @@ retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへん
enableChartsForRemoteUser: "リモートユーザーのチャートを作る" enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る" enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
reactionsDisplaySize: "リアクションの表示のでかさ"
noteIdOrUrl: "ートIDかURL" noteIdOrUrl: "ートIDかURL"
video: "動画" video: "動画"
videos: "動画" videos: "動画"
@ -1109,8 +1119,31 @@ replies: "返事"
renotes: "Renote" renotes: "Renote"
loadReplies: "返信を見るで" loadReplies: "返信を見るで"
loadConversation: "会話を見るで" loadConversation: "会話を見るで"
pinnedList: "ピン留めしはったリスト"
keepScreenOn: "デバイスの画面を常にオンにすんで"
verifiedLink: "このリンク先の所有者であることが確認されたで。" verifiedLink: "このリンク先の所有者であることが確認されたで。"
notifyNotes: "投稿を通知"
unnotifyNotes: "投稿の通知を解除すんで"
authentication: "認証"
authenticationRequiredToContinue: "続けるには認証をやってや。" authenticationRequiredToContinue: "続けるには認証をやってや。"
dateAndTime: "日時"
showRenotes: "リノートを表示"
edited: "編集し終わってる"
notificationRecieveConfig: "通知を受け取るかの設定"
mutualFollow: "お互いフォローしてんで"
fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "タイムラインに他の人への返信とかも含めんで"
hideRepliesToOthersInTimeline: "タイムラインに他の人への返信とかは見ーへんで"
showRepliesToOthersInTimelineAll: ""
externalServices: "他のサイトのサービス"
impressum: "運営者の情報"
impressumUrl: "運営者の情報URL"
impressumDescription: "ドイツなどのほんま1部の国と地域ではな、表示が義務付けられててん。(Impressum)"
privacyPolicy: "プライバシーポリシー"
privacyPolicyUrl: "プライバシーポリシーURL"
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
avatarDecorations: "アイコンデコレーション"
flip: "反転"
_announcement: _announcement:
forExistingUsers: "もうおるユーザーのみ" forExistingUsers: "もうおるユーザーのみ"
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
@ -1143,6 +1176,8 @@ _serverSettings:
appIconUsageExample: "PWAや、スマートフォンのホーム画面にブックマークとして追加された時など" appIconUsageExample: "PWAや、スマートフォンのホーム画面にブックマークとして追加された時など"
appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるさかいに、塗り潰された余白のある背景があるものが推奨されるで。" appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるさかいに、塗り潰された余白のある背景があるものが推奨されるで。"
appIconResolutionMustBe: "解像度は必ず{resolution}である必要があるで。" appIconResolutionMustBe: "解像度は必ず{resolution}である必要があるで。"
manifestJsonOverride: "manifest.jsonのオーバーライド"
shortName: "略称"
shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。" shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。"
_accountMigration: _accountMigration:
moveFrom: "別のアカウントからこのアカウントに引っ越す" moveFrom: "別のアカウントからこのアカウントに引っ越す"
@ -1398,6 +1433,9 @@ _achievements:
title: "Brain Diver" title: "Brain Diver"
description: "Brain Diverへのリンクを投稿したった" description: "Brain Diverへのリンクを投稿したった"
flavor: "Misskey-Misskey La-Tu-Ma" flavor: "Misskey-Misskey La-Tu-Ma"
_smashTestNotificationButton:
title: "テスト過剰"
description: "通知テストをごく短時間のうちに連続して行ったねん"
_role: _role:
new: "ロールの作成" new: "ロールの作成"
edit: "ロールの編集" edit: "ロールの編集"
@ -1455,6 +1493,7 @@ _role:
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。" descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
canHideAds: "広告を表示させへん" canHideAds: "広告を表示させへん"
canSearchNotes: "ノート検索を使わすかどうか" canSearchNotes: "ノート検索を使わすかどうか"
canUseTranslator: "翻訳機能の利用"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
@ -1503,6 +1542,10 @@ _ad:
reduceFrequencyOfThisAd: "この広告の表示頻度を下げるで" reduceFrequencyOfThisAd: "この広告の表示頻度を下げるで"
hide: "表示せん" hide: "表示せん"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されるで。" timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されるで。"
adsSettings: "広告配信設定"
notesPerOneAd: "リアタイ更新中に広告を出す間隔(ノートの個数な)"
setZeroToDisable: "0でリアタイ更新時の広告配信を無効にすんで"
adsTooClose: "広告を出す間隔がめっちゃ短いから、ユーザー体験が著しく損なわれる可能性があんで。"
_forgotPassword: _forgotPassword:
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。" enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。" ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
@ -1702,6 +1745,7 @@ _2fa:
step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。"
step2: "次に、ここにあるQRコードをアプリでスキャンしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな。"
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
step2Uri: "デスクトップアプリを使う時は次のURIを入れるで"
step3Title: "確認コードを入れてーや" step3Title: "確認コードを入れてーや"
step3: "アプリに表示されているトークンを入力して終わりや。" step3: "アプリに表示されているトークンを入力して終わりや。"
setupCompleted: "設定が完了したで。" setupCompleted: "設定が完了したで。"
@ -1720,6 +1764,7 @@ _2fa:
renewTOTPOk: "もっかい設定する" renewTOTPOk: "もっかい設定する"
renewTOTPCancel: "やめとく" renewTOTPCancel: "やめとく"
checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、したのバックアップコードを確認しいや。" checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、したのバックアップコードを確認しいや。"
backupCodes: "バックアップコード"
backupCodesDescription: "認証アプリが使用できんなった場合、以下のバックアップコードを使ってアカウントにアクセスできるで。これらのコードは必ず安全な場所に置いときや。各コードは一回だけ使用できるで。" backupCodesDescription: "認証アプリが使用できんなった場合、以下のバックアップコードを使ってアカウントにアクセスできるで。これらのコードは必ず安全な場所に置いときや。各コードは一回だけ使用できるで。"
backupCodeUsedWarning: "バックアップコードが使用されたで。認証アプリが使えなくなってるん場合、なるべく早く認証アプリを再設定しや。" backupCodeUsedWarning: "バックアップコードが使用されたで。認証アプリが使えなくなってるん場合、なるべく早く認証アプリを再設定しや。"
backupCodesExhaustedWarning: "バックアップコードが全て使用されたで。認証アプリを利用できん場合、これ以上アカウントにアクセスできなくなるで。認証アプリを再登録しや。" backupCodesExhaustedWarning: "バックアップコードが全て使用されたで。認証アプリを利用できん場合、これ以上アカウントにアクセスできなくなるで。認証アプリを再登録しや。"
@ -1775,6 +1820,7 @@ _antennaSources:
homeTimeline: "フォローしとるユーザーのノート" homeTimeline: "フォローしとるユーザーのノート"
users: "選らんだ一人か複数のユーザーのノート" users: "選らんだ一人か複数のユーザーのノート"
userList: "選んだリストのユーザーのノート" userList: "選んだリストのユーザーのノート"
userBlacklist: "選んだ1人か複数のユーザーのノート"
_weekday: _weekday:
sunday: "日曜日" sunday: "日曜日"
monday: "月曜日" monday: "月曜日"
@ -1874,6 +1920,7 @@ _profile:
metadataContent: "内容" metadataContent: "内容"
changeAvatar: "アバター画像を変更するで" changeAvatar: "アバター画像を変更するで"
changeBanner: "バナー画像を変更するで" changeBanner: "バナー画像を変更するで"
verifiedLinkDescription: "内容をURLに設定すると、リンク先のwebサイトに自分のプロフのリンクが含まれてる場合に所有者確認済みアイコンを表示させることができるで。"
_exportOrImport: _exportOrImport:
allNotes: "全てのノート" allNotes: "全てのノート"
favoritedNotes: "お気に入りにしたノート" favoritedNotes: "お気に入りにしたノート"
@ -1883,6 +1930,7 @@ _exportOrImport:
userLists: "リスト" userLists: "リスト"
excludeMutingUsers: "ミュートしてるユーザーは入れんとくわ" excludeMutingUsers: "ミュートしてるユーザーは入れんとくわ"
excludeInactiveUsers: "使われてなさそうなアカウントは入れんとくわ" excludeInactiveUsers: "使われてなさそうなアカウントは入れんとくわ"
withReplies: "インポートした人による返信をTLに含むようにすんで。"
_charts: _charts:
federation: "連合" federation: "連合"
apRequest: "リクエスト" apRequest: "リクエスト"
@ -1992,14 +2040,17 @@ _notification:
youReceivedFollowRequest: "フォロー許可してほしいみたいやな" youReceivedFollowRequest: "フォロー許可してほしいみたいやな"
yourFollowRequestAccepted: "フォローさせてもろたで" yourFollowRequestAccepted: "フォローさせてもろたで"
pollEnded: "アンケートの結果が出たみたいや" pollEnded: "アンケートの結果が出たみたいや"
newNote: "さらの投稿"
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで" emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
achievementEarned: "実績を獲得しとるで" achievementEarned: "実績を獲得しとるで"
testNotification: "通知テスト"
checkNotificationBehavior: "通知の表示を確かめるで" checkNotificationBehavior: "通知の表示を確かめるで"
sendTestNotification: "テスト通知を送信するで" sendTestNotification: "テスト通知を送信するで"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで" notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで"
_types: _types:
all: "すべて" all: "すべて"
note: "あんたらの新規投稿"
follow: "フォロー" follow: "フォロー"
mention: "メンション" mention: "メンション"
reply: "リプライ" reply: "リプライ"
@ -2034,6 +2085,7 @@ _deck:
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー"
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となるで" usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となるで"
flexible: "幅を自動調整"
_columns: _columns:
main: "メイン" main: "メイン"
widgets: "ウィジェット" widgets: "ウィジェット"
@ -2069,6 +2121,66 @@ _webhookSettings:
reaction: "ツッコミがあるとき~!" reaction: "ツッコミがあるとき~!"
mention: "メンションがあるとき~!" mention: "メンションがあるとき~!"
_moderationLogTypes: _moderationLogTypes:
createRole: "ロールを追加すんで"
deleteRole: "ロールほかす"
updateRole: "ロールの更新すんで"
assignRole: "ロールへアサイン"
unassignRole: "ロールのアサインほかす"
suspend: "凍結" suspend: "凍結"
unsuspend: "凍結解除"
addCustomEmoji: "自由な絵文字追加されたで"
updateCustomEmoji: "自由な絵文字更新されたで"
deleteCustomEmoji: "自由な絵文字消されたで"
updateServerSettings: "サーバー設定更新すんねん"
updateUserNote: "モデレーションノート更新"
deleteDriveFile: "ファイルをほかす"
deleteNote: "ノートを削除"
createGlobalAnnouncement: "みんなへの通告を作成したで"
createUserAnnouncement: "あんたらへの通告を作成したで"
updateGlobalAnnouncement: "みんなへの通告更新したったで"
updateUserAnnouncement: "あんたらへの通告更新したったで"
deleteGlobalAnnouncement: "みんなへの通告消したったで"
deleteUserAnnouncement: "あんたらへのお知らせを削除"
resetPassword: "パスワードをリセット" resetPassword: "パスワードをリセット"
suspendRemoteInstance: "リモートサーバーを止めんで"
unsuspendRemoteInstance: "リモートサーバーを再開すんで"
markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "苦情を解決"
createInvitation: "招待コードを作成" createInvitation: "招待コードを作成"
createAd: "広告を作んで"
deleteAd: "広告ほかす"
updateAd: "広告を更新"
createAvatarDecoration: "アイコンデコレーションを作成"
updateAvatarDecoration: "アイコンデコレーションを更新"
deleteAvatarDecoration: "アイコンデコレーションを削除"
_fileViewer:
title: "ファイルの詳しい情報"
type: "ファイルの種類"
size: "ファイルのでかさ"
url: "URL"
uploadedAt: "追加した日"
attachedNotes: "ファイルがついてきてるノート"
thisPageCanBeSeenFromTheAuthor: "このページはこのファイルをアップした人しか見れへんねん。"
_externalResourceInstaller:
title: "ほかのサイトからインストール"
checkVendorBeforeInstall: "配ってるとこが信頼できるか確認した上でインストールしてな。"
_plugin:
title: "このプラグイン、インストールする?"
metaTitle: "プラグイン情報"
_theme:
title: "このテーマインストールする?"
metaTitle: "テーマ情報"
_errors:
_pluginParseFailed:
title: "AiScriptエラー起こしてもうたねん"
description: "データは取得できたものの、AiScript解析時にエラーがあったから読み込めへんかってん。すまんが、プラグインを作った人に問い合わせてくれへんごめんな。エラーの詳細はJavaScriptコンソール読んでな。"
_pluginInstallFailed:
title: "プラグインのインストール失敗してもた"
description: "プラグインのインストール中に問題発生してもた、もう1度試してな。エラーの詳細はJavaScriptのコンソール見てや。"
_themeParseFailed:
title: "テーマ解析エラー"
description: "データは取得できたものの、テーマファイル解析時にエラーがあったから読み込めへんかってん。すまんが、テーマ作った人に問い合わせてくれへんごめんな。エラーの詳細はJavaScriptコンソール読んでな。"
_themeInstallFailed:
title: "テーマインストールに失敗してもた"
description: "テーマのインストール中に問題発生してもた、もう1度試してな。エラーの詳細はJavaScriptのコンソール見てや。"

View file

@ -162,8 +162,8 @@ cacheRemoteSensitiveFiles: "리모트의 민감한 파일을 캐시"
cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모트의 민감한 파일은 캐시하지 않고 리모트에서 직접 가져오도록 합니다." cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모트의 민감한 파일은 캐시하지 않고 리모트에서 직접 가져오도록 합니다."
flagAsBot: "나는 봇입니다" flagAsBot: "나는 봇입니다"
flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다." flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다."
flagAsCat: "나는 고양이다냥" flagAsCat: "미야아아아오오오오오오오오오옹!!!!!!!"
flagAsCatDescription: "이 계정이 고양이라면 활성화해 주세요." flagAsCatDescription: "야옹?"
flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기" flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기"
flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다." flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다."
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락" autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
@ -589,7 +589,7 @@ poll: "투표"
useCw: "내용 숨기기" useCw: "내용 숨기기"
enablePlayer: "플레이어 열기" enablePlayer: "플레이어 열기"
disablePlayer: "플레이어 닫기" disablePlayer: "플레이어 닫기"
expandTweet: "트윗 확장하기" expandTweet: "게시물 확장하기"
themeEditor: "테마 에디터" themeEditor: "테마 에디터"
description: "설명" description: "설명"
describeFile: "캡션 추가" describeFile: "캡션 추가"
@ -1122,6 +1122,7 @@ showRenotes: "리노트 표시"
edited: "수정됨" edited: "수정됨"
notificationRecieveConfig: "알림 설정" notificationRecieveConfig: "알림 설정"
mutualFollow: "맞팔로우" mutualFollow: "맞팔로우"
flip: "플립"
_announcement: _announcement:
forExistingUsers: "기존 유저에게만 알림" forExistingUsers: "기존 유저에게만 알림"
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."

View file

@ -873,6 +873,7 @@ youFollowing: "Śledzeni"
icon: "Awatar" icon: "Awatar"
replies: "Odpowiedzi" replies: "Odpowiedzi"
renotes: "Udostępnień" renotes: "Udostępnień"
flip: "Odwróć"
_role: _role:
priority: "Priorytet" priority: "Priorytet"
_priority: _priority:

View file

@ -1011,6 +1011,7 @@ icon: "Avatar"
replies: "Respostas" replies: "Respostas"
renotes: "Repostagens" renotes: "Repostagens"
keepScreenOn: "Manter a tela do dispositivo sempre ligada" keepScreenOn: "Manter a tela do dispositivo sempre ligada"
flip: "Inversão"
_initialAccountSetting: _initialAccountSetting:
followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo." followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo."
_serverSettings: _serverSettings:

View file

@ -1067,6 +1067,7 @@ doYouAgree: "Согласны?"
icon: "Аватар" icon: "Аватар"
replies: "Ответить" replies: "Ответить"
renotes: "Репост" renotes: "Репост"
flip: "Переворот"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Аккаунт успешно создан!" accountCreated: "Аккаунт успешно создан!"
letsStartAccountSetup: "Давайте настроим вашу учётную запись." letsStartAccountSetup: "Давайте настроим вашу учётную запись."

View file

@ -921,6 +921,7 @@ youFollowing: "Sledované"
icon: "Avatar" icon: "Avatar"
replies: "Odpovede" replies: "Odpovede"
renotes: "Preposlať" renotes: "Preposlať"
flip: "Preklopiť"
_role: _role:
priority: "Priorita" priority: "Priorita"
_priority: _priority:

View file

@ -1132,6 +1132,7 @@ impressumUrl: "URL อิมเพรสชั่น"
privacyPolicy: "นโยบายความเป็นส่วนตัว" privacyPolicy: "นโยบายความเป็นส่วนตัว"
privacyPolicyUrl: "URL นโยบายความเป็นส่วนตัว" privacyPolicyUrl: "URL นโยบายความเป็นส่วนตัว"
tosAndPrivacyPolicy: "เงื่อนไขในการให้บริการและนโยบายความเป็นส่วนตัว" tosAndPrivacyPolicy: "เงื่อนไขในการให้บริการและนโยบายความเป็นส่วนตัว"
flip: "ย้อนกลับ"
_announcement: _announcement:
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น" forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน" forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
@ -1747,8 +1748,8 @@ _2fa:
renewTOTPOk: "ตั้งค่าคอนฟิกใหม่" renewTOTPOk: "ตั้งค่าคอนฟิกใหม่"
renewTOTPCancel: "ไม่เป็นไร" renewTOTPCancel: "ไม่เป็นไร"
backupCodes: "รหัสสำรองข้อมูล" backupCodes: "รหัสสำรองข้อมูล"
backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีกต่อไป" backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีก"
backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้วถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น" backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้ว ถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะยังไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น"
_permissions: _permissions:
"read:account": "ดูข้อมูลบัญชีของคุณ" "read:account": "ดูข้อมูลบัญชีของคุณ"
"write:account": "แก้ไขข้อมูลบัญชีของคุณ" "write:account": "แก้ไขข้อมูลบัญชีของคุณ"

View file

@ -1,4 +1,19 @@
--- ---
_lang_: "ياپونچە" _lang_: "ياپونچە"
headlineMisskey: "خاتىرە ئارقىلىق ئۇلانغان تور"
monthAndDay: "{day}-{month}"
search: "ئىزدەش" search: "ئىزدەش"
ok: "ماقۇل"
noThankYou: "ئۇنى توختىتىڭ"
profile: "profile"
login: "كىرىش"
loggingIn: "كىرىش"
pin: "pinned"
delete: "ئۆچۈرۈش"
pinned: "pinned"
remove: "ئۆچۈرۈش"
searchByGoogle: "ئىزدەش" searchByGoogle: "ئىزدەش"
_2fa:
renewTOTPCancel: "ئۇنى توختىتىڭ"
_widgets:
profile: "profile"

View file

@ -907,6 +907,7 @@ youFollowing: "Підписки"
icon: "Аватар" icon: "Аватар"
replies: "Відповісти" replies: "Відповісти"
renotes: "Поширити" renotes: "Поширити"
flip: "Перевернути"
_achievements: _achievements:
earnedAt: "Відкрито" earnedAt: "Відкрито"
_types: _types:

View file

@ -845,6 +845,7 @@ sensitiveWords: "Ta'sirchan so'zlar"
icon: "Avatar" icon: "Avatar"
replies: "Javoblar" replies: "Javoblar"
renotes: "Qayta qayd etish" renotes: "Qayta qayd etish"
flip: "Teskari"
_achievements: _achievements:
_types: _types:
_viewInstanceChart: _viewInstanceChart:

View file

@ -1047,6 +1047,7 @@ loadReplies: "Hiển thị các trả lời"
pinnedList: "Các mục đã được ghim" pinnedList: "Các mục đã được ghim"
keepScreenOn: "Giữ màn hình luôn bật" keepScreenOn: "Giữ màn hình luôn bật"
verifiedLink: "Chúng tôi đã xác nhận bạn là chủ sở hữu của đường dẫn này" verifiedLink: "Chúng tôi đã xác nhận bạn là chủ sở hữu của đường dẫn này"
flip: "Lật"
_announcement: _announcement:
forExistingUsers: "Chỉ những người dùng đã tồn tại" forExistingUsers: "Chỉ những người dùng đã tồn tại"
forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó." forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó."

View file

@ -195,6 +195,7 @@ perHour: "每小时"
perDay: "每天" perDay: "每天"
stopActivityDelivery: "停止发送活动" stopActivityDelivery: "停止发送活动"
blockThisInstance: "阻止此服务器向本服务器推流" blockThisInstance: "阻止此服务器向本服务器推流"
silenceThisInstance: "使服务器静音"
operations: "操作" operations: "操作"
software: "软件" software: "软件"
version: "版本" version: "版本"
@ -214,6 +215,8 @@ clearCachedFiles: "清除缓存"
clearCachedFilesConfirm: "确定要清除缓存文件?" clearCachedFilesConfirm: "确定要清除缓存文件?"
blockedInstances: "被封锁的服务器" blockedInstances: "被封锁的服务器"
blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。" blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
silencedInstances: "沉默的服务器"
silencedInstancesDescription: "设置要静音的服务器的主机,以换行符分隔。属于静默服务器的所有帐户都将被视为“静默”,所有关注都将成为请求,并且您将无法提及非关注者的本地帐户。被阻止的实例不受影响。"
muteAndBlock: "屏蔽/拉黑" muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户" mutedUsers: "已屏蔽用户"
blockedUsers: "已拉黑的用户" blockedUsers: "已拉黑的用户"
@ -1128,6 +1131,7 @@ mutualFollow: "互相关注"
fileAttachedOnly: "仅限媒体" fileAttachedOnly: "仅限媒体"
showRepliesToOthersInTimeline: "在时间线上显示给其他人的回复" showRepliesToOthersInTimeline: "在时间线上显示给其他人的回复"
hideRepliesToOthersInTimeline: "在时间线上隐藏给其他人的回复" hideRepliesToOthersInTimeline: "在时间线上隐藏给其他人的回复"
flip: "翻转"
_announcement: _announcement:
forExistingUsers: "仅限现有用户" forExistingUsers: "仅限现有用户"
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
@ -2127,3 +2131,6 @@ _moderationLogTypes:
createAd: "创建了广告" createAd: "创建了广告"
deleteAd: "删除了广告" deleteAd: "删除了广告"
updateAd: "更新了广告" updateAd: "更新了广告"
_fileViewer:
url: "URL"
uploadedAt: "添加日期"

View file

@ -195,6 +195,7 @@ perHour: "每小時"
perDay: "每日" perDay: "每日"
stopActivityDelivery: "停止發送活動" stopActivityDelivery: "停止發送活動"
blockThisInstance: "封鎖此伺服器" blockThisInstance: "封鎖此伺服器"
silenceThisInstance: "禁言此伺服器"
operations: "操作" operations: "操作"
software: "軟體" software: "軟體"
version: "版本" version: "版本"
@ -214,6 +215,8 @@ clearCachedFiles: "清除快取資料"
clearCachedFilesConfirm: "確定要清除所有遠端暫存資料嗎?" clearCachedFilesConfirm: "確定要清除所有遠端暫存資料嗎?"
blockedInstances: "已封鎖的伺服器" blockedInstances: "已封鎖的伺服器"
blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。" blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。"
silencedInstances: "被禁言的伺服器"
silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。"
muteAndBlock: "靜音和封鎖" muteAndBlock: "靜音和封鎖"
mutedUsers: "被靜音的使用者" mutedUsers: "被靜音的使用者"
blockedUsers: "被封鎖的使用者" blockedUsers: "被封鎖的使用者"
@ -531,6 +534,7 @@ serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄" deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框" showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框"
showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)" showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)"
withRepliesByDefaultForNewlyFollowed: "在追隨其他人後,預設在時間軸納入回覆的貼文"
newNoteRecived: "發現新貼文" newNoteRecived: "發現新貼文"
sounds: "音效" sounds: "音效"
sound: "音效" sound: "音效"
@ -975,6 +979,7 @@ assign: "指派"
unassign: "取消指派" unassign: "取消指派"
color: "顏色" color: "顏色"
manageCustomEmojis: "管理自訂表情符號" manageCustomEmojis: "管理自訂表情符號"
manageAvatarDecorations: "管理頭像裝飾"
youCannotCreateAnymore: "您無法再建立更多了。" youCannotCreateAnymore: "您無法再建立更多了。"
cannotPerformTemporary: "暫時無法進行" cannotPerformTemporary: "暫時無法進行"
cannotPerformTemporaryDescription: "由於超過操作次數限制,因此暫時無法進行。請稍後再嘗試。" cannotPerformTemporaryDescription: "由於超過操作次數限制,因此暫時無法進行。請稍後再嘗試。"
@ -1128,6 +1133,10 @@ mutualFollow: "互相追隨"
fileAttachedOnly: "顯示包含附件的貼文" fileAttachedOnly: "顯示包含附件的貼文"
showRepliesToOthersInTimeline: "顯示給其他人的回覆" showRepliesToOthersInTimeline: "顯示給其他人的回覆"
hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆" hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
showRepliesToOthersInTimelineAll: "在時間軸包含追隨中所有人的回覆"
hideRepliesToOthersInTimelineAll: "在時間軸不包含追隨中所有人的回覆"
confirmShowRepliesAll: "進行此操作後無法復原。您真的希望時間軸「包含」您目前追隨的所有人的回覆嗎?"
confirmHideRepliesAll: "進行此操作後無法復原。您真的希望時間軸「不包含」您目前追隨的所有人的回覆嗎?"
externalServices: "外部服務" externalServices: "外部服務"
impressum: "營運者資訊" impressum: "營運者資訊"
impressumUrl: "營運者資訊網址" impressumUrl: "營運者資訊網址"
@ -1135,6 +1144,16 @@ impressumDescription: "在德國與部份地區必須要明確顯示營運者資
privacyPolicy: "隱私政策" privacyPolicy: "隱私政策"
privacyPolicyUrl: "隱私政策網址" privacyPolicyUrl: "隱私政策網址"
tosAndPrivacyPolicy: "服務條款和隱私政策" tosAndPrivacyPolicy: "服務條款和隱私政策"
avatarDecorations: "頭像裝飾"
attach: "裝上"
detach: "取下"
angle: "角度"
flip: "翻轉"
showAvatarDecorations: "顯示頭像裝飾"
releaseToRefresh: "放開以更新內容"
refreshing: "載入更新中"
pullDownToRefresh: "往下拉來更新內容"
disableStreamingTimeline: "停用時間軸的即時更新"
_announcement: _announcement:
forExistingUsers: "僅限既有的使用者" forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
@ -1170,6 +1189,7 @@ _serverSettings:
manifestJsonOverride: "覆寫 manifest.json" manifestJsonOverride: "覆寫 manifest.json"
shortName: "簡稱" shortName: "簡稱"
shortNameDescription: "如果伺服器的正式名稱很長,可用簡稱或通稱代替。" shortNameDescription: "如果伺服器的正式名稱很長,可用簡稱或通稱代替。"
fanoutTimelineDescription: "如果啟用的話檢索各個時間軸的性能會顯著提昇資料庫的負荷也會減少。不過Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。"
_accountMigration: _accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶" moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名" moveFromSub: "為另一個帳戶建立別名"
@ -1470,6 +1490,7 @@ _role:
inviteLimitCycle: "邀請碼的發放間隔" inviteLimitCycle: "邀請碼的發放間隔"
inviteExpirationTime: "邀請碼的有效日期" inviteExpirationTime: "邀請碼的有效日期"
canManageCustomEmojis: "管理自訂表情符號" canManageCustomEmojis: "管理自訂表情符號"
canManageAvatarDecorations: "管理頭像裝飾"
driveCapacity: "雲端硬碟容量" driveCapacity: "雲端硬碟容量"
alwaysMarkNsfw: "總是將檔案標記為NSFW" alwaysMarkNsfw: "總是將檔案標記為NSFW"
pinMax: "置頂貼文的最大數量" pinMax: "置頂貼文的最大數量"
@ -1589,6 +1610,7 @@ _aboutMisskey:
donate: "贊助 Misskey" donate: "贊助 Misskey"
morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
patrons: "贊助者" patrons: "贊助者"
projectMembers: "專案成員"
_displayOfSensitiveMedia: _displayOfSensitiveMedia:
respect: "隱藏敏感檔案" respect: "隱藏敏感檔案"
ignore: "顯示敏感檔案" ignore: "顯示敏感檔案"
@ -1921,6 +1943,7 @@ _exportOrImport:
userLists: "清單" userLists: "清單"
excludeMutingUsers: "排除被靜音的使用者" excludeMutingUsers: "排除被靜音的使用者"
excludeInactiveUsers: "排除不活躍帳戶" excludeInactiveUsers: "排除不活躍帳戶"
withReplies: "將被匯入的追隨中清單的貼文回覆包含在時間軸"
_charts: _charts:
federation: "聯邦宇宙" federation: "聯邦宇宙"
apRequest: "請求" apRequest: "請求"
@ -2141,6 +2164,9 @@ _moderationLogTypes:
createAd: "建立廣告" createAd: "建立廣告"
deleteAd: "刪除廣告" deleteAd: "刪除廣告"
updateAd: "更新廣告" updateAd: "更新廣告"
createAvatarDecoration: "建立頭像裝飾"
updateAvatarDecoration: "更新頭像裝飾"
deleteAvatarDecoration: "刪除頭像裝飾"
_fileViewer: _fileViewer:
title: "檔案詳細資訊" title: "檔案詳細資訊"
type: "檔案類型 " type: "檔案類型 "
@ -2149,3 +2175,44 @@ _fileViewer:
uploadedAt: "加入日期" uploadedAt: "加入日期"
attachedNotes: "含有附件的貼文" attachedNotes: "含有附件的貼文"
thisPageCanBeSeenFromTheAuthor: "本頁面僅限上傳了這個檔案的使用者可以檢視。" thisPageCanBeSeenFromTheAuthor: "本頁面僅限上傳了這個檔案的使用者可以檢視。"
_externalResourceInstaller:
title: "從外部網站安裝"
checkVendorBeforeInstall: "安裝前請確認提供者是可信賴的。"
_plugin:
title: "要安裝此外掛嘛?"
metaTitle: "外掛資訊"
_theme:
title: "要安裝此外觀主題嘛?"
metaTitle: "外觀主題資訊"
_meta:
base: "基本配色方案"
_vendorInfo:
title: "提供者資訊"
endpoint: "引用端點"
hashVerify: "確認檔案的完整性"
_errors:
_invalidParams:
title: "缺少參數"
description: "缺少從外部網站取得資料的必要資訊。請檢查 URL 是否正確。"
_resourceTypeNotSupported:
title: "不支援此外部資源。"
description: "不支援從此外部網站取得的資源類型。請聯絡網站管理員。"
_failedToFetch:
title: "無法取得資料"
fetchErrorDescription: "與外部站點的通訊失敗。如果重試後問題仍然存在,請聯絡網站管理員。"
parseErrorDescription: "無法讀取從外部站點取得的資料。請聯絡網站管理員。"
_hashUnmatched:
title: "無法取得正確資料"
description: "所提供資料的完整性驗證失敗。出於安全原因,安裝無法繼續。請聯絡網站管理員。"
_pluginParseFailed:
title: "AiScript 錯誤"
description: "已取得資料但解析 AiScript 時發生錯誤,導致無法載入。請聯絡外掛作者。請檢查 Javascript 控制台以取得錯誤詳細資訊。"
_pluginInstallFailed:
title: "外掛安裝失敗"
description: "安裝插件時出現問題。請再試一次。請參閱 Javascript 控制台以取得錯誤詳細資訊。"
_themeParseFailed:
title: "外觀主題解析錯誤"
description: "已取得資料但解析外觀主題時發生錯誤,導致無法載入。請聯絡主題作者。請檢查 Javascript 控制台以取得錯誤詳細資訊。"
_themeInstallFailed:
title: "無法安裝外觀主題"
description: "安裝外觀主題時出現問題。請再試一次。請參閱 Javascript 控制台以取得錯誤詳細資訊。"

View file

@ -1,12 +1,12 @@
{ {
"name": "sharkey", "name": "sharkey",
"version": "2023.10.3.beta1", "version": "2023.11.0.beta2",
"codename": "shonk", "codename": "shonk",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/transfem-org/sharkey.git" "url": "https://github.com/transfem-org/sharkey.git"
}, },
"packageManager": "pnpm@8.9.2", "packageManager": "pnpm@8.10.0",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",
@ -22,6 +22,7 @@
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
"init": "pnpm migrate", "init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm migrate", "migrate": "cd packages/backend && pnpm migrate",
"revert": "cd packages/backend && pnpm revert",
"check:connect": "cd packages/backend && pnpm check:connect", "check:connect": "cd packages/backend && pnpm check:connect",
"migrateandstart": "pnpm migrate && pnpm start", "migrateandstart": "pnpm migrate && pnpm start",
"watch": "pnpm dev", "watch": "pnpm dev",
@ -47,15 +48,15 @@
"cssnano": "6.0.1", "cssnano": "6.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.31", "postcss": "8.4.31",
"terser": "5.21.0", "terser": "5.22.0",
"typescript": "5.2.2" "typescript": "5.2.2"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "6.8.0", "@typescript-eslint/eslint-plugin": "6.9.0",
"@typescript-eslint/parser": "6.8.0", "@typescript-eslint/parser": "6.9.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.3.1", "cypress": "13.3.3",
"eslint": "8.51.0", "eslint": "8.52.0",
"start-server-and-test": "2.0.1" "start-server-and-test": "2.0.1"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NoteReactionAndUserPairCache1697673894459 {
name = 'NoteReactionAndUserPairCache1697673894459'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "reactionAndUserPairCache" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "reactionAndUserPairCache"`);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AvatarDecoration1697847397844 {
name = 'AvatarDecoration1697847397844'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "avatar_decoration" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "url" character varying(1024) NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(2048) NOT NULL, "roleIdsThatCanBeUsedThisDecoration" character varying(128) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_b6de9296f6097078e1dc53f7603" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" character varying(512) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`);
await queryRunner.query(`DROP TABLE "avatar_decoration"`);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AvatarDecoration21697941908548 {
name = 'AvatarDecoration21697941908548'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" jsonb NOT NULL DEFAULT '[]'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" character varying(512) array NOT NULL DEFAULT '{}'`);
}
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class EnableFtt1698041201306 {
name = 'EnableFtt1698041201306'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimeline" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimeline"`);
}
}

View file

@ -10,6 +10,7 @@
"start": "node ./built/index.js", "start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js",
"migrate": "pnpm typeorm migration:run -d ormconfig.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
"check:connect": "node ./check_connect.js", "check:connect": "node ./check_connect.js",
"build": "swc src -d built -D", "build": "swc src -d built -D",
"watch:swc": "swc src -d built -D -w", "watch:swc": "swc src -d built -D -w",
@ -75,10 +76,10 @@
"@nestjs/core": "10.2.7", "@nestjs/core": "10.2.7",
"@nestjs/testing": "10.2.7", "@nestjs/testing": "10.2.7",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.3.2", "@simplewebauthn/server": "8.3.4",
"@sinonjs/fake-timers": "11.1.0", "@sinonjs/fake-timers": "11.2.2",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.93", "@swc/core": "1.3.95",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "6.0.1", "archiver": "6.0.1",
@ -87,7 +88,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "4.12.4", "bullmq": "4.12.6",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.1", "cbor": "9.0.1",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -98,8 +99,8 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.24.2",
"fastify-multer": "^2.0.3", "fastify-multer": "^2.0.3",
"fastify": "4.24.3",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.5.0", "file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
@ -127,7 +128,7 @@
"nanoid": "5.0.2", "nanoid": "5.0.2",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.6", "nodemailer": "6.9.7",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
@ -145,7 +146,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.20.3", "re2": "1.20.5",
"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",
@ -158,7 +159,7 @@
"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.21.12", "systeminformation": "5.21.15",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.8", "tsc-alias": "1.8.8",
@ -175,7 +176,7 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@simplewebauthn/typescript-types": "8.0.0", "@simplewebauthn/typescript-types": "8.3.4",
"@swc/jest": "0.2.29", "@swc/jest": "0.2.29",
"@types/accepts": "1.3.6", "@types/accepts": "1.3.6",
"@types/archiver": "5.3.4", "@types/archiver": "5.3.4",
@ -184,45 +185,45 @@
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/color-convert": "2.0.2", "@types/color-convert": "2.0.2",
"@types/content-disposition": "0.5.7", "@types/content-disposition": "0.5.7",
"@types/fluent-ffmpeg": "2.1.22", "@types/fluent-ffmpeg": "2.1.23",
"@types/http-link-header": "1.0.3", "@types/http-link-header": "1.0.4",
"@types/jest": "29.5.5", "@types/jest": "29.5.6",
"@types/js-yaml": "4.0.7", "@types/js-yaml": "4.0.8",
"@types/jsdom": "21.1.3", "@types/jsdom": "21.1.4",
"@types/jsonld": "1.5.10", "@types/jsonld": "1.5.11",
"@types/jsrsasign": "10.5.9", "@types/jsrsasign": "10.5.11",
"@types/mime-types": "2.1.2", "@types/mime-types": "2.1.3",
"@types/ms": "0.7.32", "@types/ms": "0.7.33",
"@types/node": "20.8.6", "@types/node": "20.8.9",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.11", "@types/nodemailer": "6.4.13",
"@types/oauth": "0.9.2", "@types/oauth": "0.9.3",
"@types/oauth2orize": "1.11.1", "@types/oauth2orize": "1.11.2",
"@types/oauth2orize-pkce": "0.1.0", "@types/oauth2orize-pkce": "0.1.1",
"@types/pg": "8.10.5", "@types/pg": "8.10.7",
"@types/pug": "2.0.7", "@types/pug": "2.0.8",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.1",
"@types/qrcode": "1.5.2", "@types/qrcode": "1.5.4",
"@types/random-seed": "0.3.3", "@types/random-seed": "0.3.4",
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.5",
"@types/rename": "1.0.5", "@types/rename": "1.0.6",
"@types/sanitize-html": "2.9.2", "@types/sanitize-html": "2.9.3",
"@types/semver": "7.5.3", "@types/semver": "7.5.4",
"@types/sharp": "0.32.0", "@types/sharp": "0.32.0",
"@types/simple-oauth2": "5.0.5", "@types/simple-oauth2": "5.0.6",
"@types/sinonjs__fake-timers": "8.1.3", "@types/sinonjs__fake-timers": "8.1.4",
"@types/tinycolor2": "1.4.4", "@types/tinycolor2": "1.4.5",
"@types/tmp": "0.2.4", "@types/tmp": "0.2.5",
"@types/vary": "1.1.2",
"@types/web-push": "3.6.2",
"@types/uuid": "^9.0.4", "@types/uuid": "^9.0.4",
"@types/vary": "1.1.1", "@types/ws": "8.5.8",
"@types/web-push": "3.6.1", "@typescript-eslint/eslint-plugin": "6.9.0",
"@types/ws": "8.5.7", "@typescript-eslint/parser": "6.9.0",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.51.0", "eslint": "8.52.0",
"eslint-plugin-import": "2.28.1", "eslint-plugin-import": "2.29.0",
"execa": "8.0.1", "execa": "8.0.1",
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",

View file

@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { AvatarDecorationsRepository, MiAvatarDecoration, MiUser } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable()
export class AvatarDecorationService implements OnApplicationShutdown {
public cache: MemorySingleCache<MiAvatarDecoration[]>;
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.avatarDecorationsRepository)
private avatarDecorationsRepository: AvatarDecorationsRepository,
private idService: IdService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'avatarDecorationCreated':
case 'avatarDecorationUpdated':
case 'avatarDecorationDeleted': {
this.cache.delete();
break;
}
default:
break;
}
}
}
@bindThis
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
const created = await this.avatarDecorationsRepository.insert({
id: this.idService.gen(),
...options,
}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
if (moderator) {
this.moderationLogService.log(moderator, 'createAvatarDecoration', {
avatarDecorationId: created.id,
avatarDecoration: created,
});
}
return created;
}
@bindThis
public async update(id: MiAvatarDecoration['id'], params: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<void> {
const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id });
const date = new Date();
await this.avatarDecorationsRepository.update(avatarDecoration.id, {
updatedAt: date,
...params,
});
const updated = await this.avatarDecorationsRepository.findOneByOrFail({ id: avatarDecoration.id });
this.globalEventService.publishInternalEvent('avatarDecorationUpdated', updated);
if (moderator) {
this.moderationLogService.log(moderator, 'updateAvatarDecoration', {
avatarDecorationId: avatarDecoration.id,
before: avatarDecoration,
after: updated,
});
}
}
@bindThis
public async delete(id: MiAvatarDecoration['id'], moderator?: MiUser): Promise<void> {
const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id });
await this.avatarDecorationsRepository.delete({ id: avatarDecoration.id });
this.globalEventService.publishInternalEvent('avatarDecorationDeleted', avatarDecoration);
if (moderator) {
this.moderationLogService.log(moderator, 'deleteAvatarDecoration', {
avatarDecorationId: avatarDecoration.id,
avatarDecoration: avatarDecoration,
});
}
}
@bindThis
public async getAll(noCache = false): Promise<MiAvatarDecoration[]> {
if (noCache) {
this.cache.delete();
}
return this.cache.fetch(() => this.avatarDecorationsRepository.find());
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View file

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js'; import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -26,7 +26,6 @@ export class CacheService implements OnApplicationShutdown {
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>; public renoteMutingsCache: RedisKVCache<Set<string>>;
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>; public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
constructor( constructor(
@Inject(DI.redis) @Inject(DI.redis)
@ -53,9 +52,6 @@ export class CacheService implements OnApplicationShutdown {
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
@ -150,13 +146,7 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => JSON.parse(value), fromRedisConverter: (value) => JSON.parse(value),
}); });
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', { // NOTE: チャンネルのフォロー状況キャッシュはChannelFollowingServiceで行っている
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.redisForSub.on('message', this.onMessage); this.redisForSub.on('message', this.onMessage);
} }
@ -221,7 +211,6 @@ export class CacheService implements OnApplicationShutdown {
this.userBlockedCache.dispose(); this.userBlockedCache.dispose();
this.renoteMutingsCache.dispose(); this.renoteMutingsCache.dispose();
this.userFollowingsCache.dispose(); this.userFollowingsCache.dispose();
this.userFollowingChannelsCache.dispose();
} }
@bindThis @bindThis

View file

@ -0,0 +1,104 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { ChannelFollowingsRepository } from '@/models/_.js';
import { MiChannel } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js';
import type { MiLocalUser } from '@/models/User.js';
import { RedisKVCache } from '@/misc/cache.js';
@Injectable()
export class ChannelFollowingService implements OnModuleInit {
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.channelFollowingsRepository.find({
where: { followerId: key },
select: ['followeeId'],
}).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.redisForSub.on('message', this.onMessage);
}
onModuleInit() {
}
@bindThis
public async follow(
requestUser: MiLocalUser,
targetChannel: MiChannel,
): Promise<void> {
await this.channelFollowingsRepository.insert({
id: this.idService.gen(),
followerId: requestUser.id,
followeeId: targetChannel.id,
});
this.globalEventService.publishInternalEvent('followChannel', {
userId: requestUser.id,
channelId: targetChannel.id,
});
}
@bindThis
public async unfollow(
requestUser: MiLocalUser,
targetChannel: MiChannel,
): Promise<void> {
await this.channelFollowingsRepository.delete({
followerId: requestUser.id,
followeeId: targetChannel.id,
});
this.globalEventService.publishInternalEvent('unfollowChannel', {
userId: requestUser.id,
channelId: targetChannel.id,
});
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'followChannel': {
this.userFollowingChannelsCache.refresh(body.userId);
break;
}
case 'unfollowChannel': {
this.userFollowingChannelsCache.delete(body.userId);
break;
}
}
}
}
@bindThis
public dispose(): void {
this.userFollowingChannelsCache.dispose();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View file

@ -11,6 +11,7 @@ import { AnnouncementService } from './AnnouncementService.js';
import { AntennaService } from './AntennaService.js'; import { AntennaService } from './AntennaService.js';
import { AppLockService } from './AppLockService.js'; import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js'; import { AchievementService } from './AchievementService.js';
import { AvatarDecorationService } from './AvatarDecorationService.js';
import { CaptchaService } from './CaptchaService.js'; import { CaptchaService } from './CaptchaService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js';
import { CustomEmojiService } from './CustomEmojiService.js'; import { CustomEmojiService } from './CustomEmojiService.js';
@ -63,6 +64,7 @@ import { SearchService } from './SearchService.js';
import { ClipService } from './ClipService.js'; import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js'; import { FeaturedService } from './FeaturedService.js';
import { FunoutTimelineService } from './FunoutTimelineService.js'; import { FunoutTimelineService } from './FunoutTimelineService.js';
import { ChannelFollowingService } from './ChannelFollowingService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js'; import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.js'; import NotesChart from './chart/charts/notes.js';
@ -141,6 +143,7 @@ const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExis
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
@ -193,6 +196,7 @@ const $SearchService: Provider = { provide: 'SearchService', useExisting: Search
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService }; const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
@ -275,6 +279,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AntennaService, AntennaService,
AppLockService, AppLockService,
AchievementService, AchievementService,
AvatarDecorationService,
CaptchaService, CaptchaService,
CreateSystemUserService, CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
@ -327,6 +332,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ClipService, ClipService,
FeaturedService, FeaturedService,
FunoutTimelineService, FunoutTimelineService,
ChannelFollowingService,
ChartLoggerService, ChartLoggerService,
FederationChart, FederationChart,
NotesChart, NotesChart,
@ -402,6 +408,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AntennaService, $AntennaService,
$AppLockService, $AppLockService,
$AchievementService, $AchievementService,
$AvatarDecorationService,
$CaptchaService, $CaptchaService,
$CreateSystemUserService, $CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
@ -454,6 +461,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ClipService, $ClipService,
$FeaturedService, $FeaturedService,
$FunoutTimelineService, $FunoutTimelineService,
$ChannelFollowingService,
$ChartLoggerService, $ChartLoggerService,
$FederationChart, $FederationChart,
$NotesChart, $NotesChart,
@ -530,6 +538,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AntennaService, AntennaService,
AppLockService, AppLockService,
AchievementService, AchievementService,
AvatarDecorationService,
CaptchaService, CaptchaService,
CreateSystemUserService, CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
@ -582,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ClipService, ClipService,
FeaturedService, FeaturedService,
FunoutTimelineService, FunoutTimelineService,
ChannelFollowingService,
FederationChart, FederationChart,
NotesChart, NotesChart,
UsersChart, UsersChart,
@ -656,6 +666,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AntennaService, $AntennaService,
$AppLockService, $AppLockService,
$AchievementService, $AchievementService,
$AvatarDecorationService,
$CaptchaService, $CaptchaService,
$CreateSystemUserService, $CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
@ -708,6 +719,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ClipService, $ClipService,
$FeaturedService, $FeaturedService,
$FunoutTimelineService, $FunoutTimelineService,
$ChannelFollowingService,
$FederationChart, $FederationChart,
$NotesChart, $NotesChart,
$UsersChart, $UsersChart,

View file

@ -52,7 +52,7 @@ export class FeaturedService {
`${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES'); `${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES');
redisPipeline.zrange( redisPipeline.zrange(
`${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES'); `${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES');
const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => r[1] as string[]) : [[], []]); const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => (r[1] ?? []) as string[]) : [[], []]);
const ranking = new Map<string, number>(); const ranking = new Map<string, number>();
for (let i = 0; i < currentRankingResult.length; i += 2) { for (let i = 0; i < currentRankingResult.length; i += 2) {

View file

@ -18,7 +18,7 @@ import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js'; import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js'; import type { MiWebhook } from '@/models/Webhook.js';
import type { MiMeta } from '@/models/Meta.js'; import type { MiMeta } from '@/models/Meta.js';
import { MiRole, MiRoleAssignment } from '@/models/_.js'; import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -77,7 +77,13 @@ export interface MainEventTypes {
unreadAntenna: MiAntenna; unreadAntenna: MiAntenna;
readAllAnnouncements: undefined; readAllAnnouncements: undefined;
myTokenRegenerated: undefined; myTokenRegenerated: undefined;
signin: MiSignin; signin: {
id: MiSignin['id'];
createdAt: string;
ip: string;
headers: Record<string, any>;
success: boolean;
};
registryUpdated: { registryUpdated: {
scope?: string[]; scope?: string[];
key: string; key: string;
@ -188,6 +194,9 @@ export interface InternalEventTypes {
antennaCreated: MiAntenna; antennaCreated: MiAntenna;
antennaDeleted: MiAntenna; antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna; antennaUpdated: MiAntenna;
avatarDecorationCreated: MiAvatarDecoration;
avatarDecorationDeleted: MiAvatarDecoration;
avatarDecorationUpdated: MiAvatarDecoration;
metaUpdated: MiMeta; metaUpdated: MiMeta;
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };

View file

@ -55,8 +55,8 @@ import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { nyaize } from '@/misc/nyaize.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -217,6 +217,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
private utilityService: UtilityService, private utilityService: UtilityService,
private userBlockingService: UserBlockingService,
) { } ) { }
@bindThis @bindThis
@ -228,8 +229,6 @@ export class NoteCreateService implements OnApplicationShutdown {
isCat: MiUser['isCat']; isCat: MiUser['isCat'];
speakAsCat: MiUser['speakAsCat']; speakAsCat: MiUser['speakAsCat'];
}, data: Option, silent = false): Promise<MiNote> { }, data: Option, silent = false): Promise<MiNote> {
let patsedText: mfm.MfmNode[] | null = null;
// チャンネル外にリプライしたら対象のスコープに合わせる // チャンネル外にリプライしたら対象のスコープに合わせる
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
@ -296,6 +295,18 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
// Check blocking
if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) {
if (data.renote.userHost === null) {
if (data.renote.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
if (blocked) {
throw new Error('blocked');
}
}
}
}
// 返信対象がpublicではないならhomeにする // 返信対象がpublicではないならhomeにする
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') { if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
data.visibility = 'home'; data.visibility = 'home';
@ -316,25 +327,6 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
} }
data.text = data.text.trim(); data.text = data.text.trim();
if (user.isCat && user.speakAsCat) {
patsedText = mfm.parse(data.text);
function nyaizeNode(node: mfm.MfmNode) {
if (node.type === 'quote') return;
if (node.type === 'text') {
node.props.text = nyaize(node.props.text);
}
if (node.children) {
for (const child of node.children) {
nyaizeNode(child);
}
}
}
for (const node of patsedText) {
nyaizeNode(node);
}
data.text = mfm.toString(patsedText);
}
} else { } else {
data.text = null; data.text = null;
} }
@ -345,7 +337,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Parse MFM if needed // Parse MFM if needed
if (!tags || !emojis || !mentionedUsers) { if (!tags || !emojis || !mentionedUsers) {
const tokens = patsedText ?? (data.text ? mfm.parse(data.text)! : []); const tokens = (data.text ? mfm.parse(data.text)! : []);
const cwTokens = data.cw ? mfm.parse(data.cw)! : []; const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
const choiceTokens = data.poll && data.poll.choices const choiceTokens = data.poll && data.poll.choices
? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) ? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
@ -598,7 +590,7 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
// Pack the note // Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true }); const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
this.globalEventService.publishNotesStream(noteObj); this.globalEventService.publishNotesStream(noteObj);
@ -861,6 +853,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis @bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (!meta.enableFanoutTimeline) return;
const r = this.redisForTimelines.pipeline(); const r = this.redisForTimelines.pipeline();
@ -904,7 +897,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (note.visibility === 'followers') { if (note.visibility === 'followers') {
// TODO: 重そうだから何とかしたい Set 使う? // TODO: 重そうだから何とかしたい Set 使う?
userListMemberships = userListMemberships.filter(x => followings.some(f => f.followerId === x.userListUserId)); userListMemberships = userListMemberships.filter(x => x.userListUserId === user.id || followings.some(f => f.followerId === x.userListUserId));
} }
// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする // TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする

View file

@ -24,6 +24,7 @@ import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js'; import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { isPureRenote } from '@/misc/is-pure-renote.js';
@Injectable() @Injectable()
export class NoteDeleteService { export class NoteDeleteService {
@ -84,8 +85,8 @@ export class NoteDeleteService {
if (this.userEntityService.isLocalUser(user) && !note.localOnly) { if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
let renote: MiNote | null = null; let renote: MiNote | null = null;
// if deletd note is renote // if deleted note is renote
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { if (isPureRenote(note)) {
renote = await this.notesRepository.findOneBy({ renote = await this.notesRepository.findOneBy({
id: note.renoteId, id: note.renoteId,
}); });

View file

@ -40,7 +40,7 @@ export class QueryService {
) { ) {
} }
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> { public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> {
if (sinceId && untilId) { if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });

View file

@ -30,6 +30,7 @@ import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
const FALLBACK = '❤'; const FALLBACK = '❤';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
@ -187,6 +188,9 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
.set({ .set({
reactions: () => sql, reactions: () => sql,
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
} : {}),
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
@ -293,6 +297,7 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
.set({ .set({
reactions: () => sql, reactions: () => sql,
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();

View file

@ -32,6 +32,7 @@ export type RolePolicies = {
inviteLimitCycle: number; inviteLimitCycle: number;
inviteExpirationTime: number; inviteExpirationTime: number;
canManageCustomEmojis: boolean; canManageCustomEmojis: boolean;
canManageAvatarDecorations: boolean;
canSearchNotes: boolean; canSearchNotes: boolean;
canUseTranslator: boolean; canUseTranslator: boolean;
canHideAds: boolean; canHideAds: boolean;
@ -57,6 +58,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
inviteLimitCycle: 60 * 24 * 7, inviteLimitCycle: 60 * 24 * 7,
inviteExpirationTime: 0, inviteExpirationTime: 0,
canManageCustomEmojis: false, canManageCustomEmojis: false,
canManageAvatarDecorations: false,
canSearchNotes: false, canSearchNotes: false,
canUseTranslator: true, canUseTranslator: true,
canHideAds: false, canHideAds: false,
@ -227,6 +229,12 @@ export class RoleService implements OnApplicationShutdown {
} }
} }
@bindThis
public async getRoles() {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
return roles;
}
@bindThis @bindThis
public async getUserAssigns(userId: MiUser['id']) { public async getUserAssigns(userId: MiUser['id']) {
const now = Date.now(); const now = Date.now();
@ -300,6 +308,7 @@ export class RoleService implements OnApplicationShutdown {
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)), inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),

View file

@ -497,6 +497,7 @@ export class ApRendererService {
preferredUsername: user.username, preferredUsername: user.username,
name: user.name, name: user.name,
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
_misskey_summary: profile.description,
icon: avatar ? this.renderImage(avatar) : null, icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null, image: banner ? this.renderImage(banner) : null,
backgroundUrl: background ? this.renderImage(background) : null, backgroundUrl: background ? this.renderImage(background) : null,
@ -796,6 +797,7 @@ export class ApRendererService {
'_misskey_quote': 'misskey:_misskey_quote', '_misskey_quote': 'misskey:_misskey_quote',
'_misskey_reaction': 'misskey:_misskey_reaction', '_misskey_reaction': 'misskey:_misskey_reaction',
'_misskey_votes': 'misskey:_misskey_votes', '_misskey_votes': 'misskey:_misskey_votes',
'_misskey_summary': 'misskey:_misskey_summary',
'isCat': 'misskey:isCat', 'isCat': 'misskey:isCat',
// Firefish // Firefish
firefish: "https://joinfirefish.org/ns#", firefish: "https://joinfirefish.org/ns#",

View file

@ -334,9 +334,17 @@ export class ApPersonService implements OnModuleInit {
emojis, emojis,
})) as MiRemoteUser; })) as MiRemoteUser;
let _description: string | null = null;
if (person._misskey_summary) {
_description = truncate(person._misskey_summary, summaryLength);
} else if (person.summary) {
_description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag);
}
await transactionalEntityManager.save(new MiUserProfile({ await transactionalEntityManager.save(new MiUserProfile({
userId: user.id, userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: _description,
url, url,
fields, fields,
birthday: bday?.[0] ?? null, birthday: bday?.[0] ?? null,
@ -505,10 +513,18 @@ export class ApPersonService implements OnModuleInit {
}); });
} }
let _description: string | null = null;
if (person._misskey_summary) {
_description = truncate(person._misskey_summary, summaryLength);
} else if (person.summary) {
_description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag);
}
await this.userProfilesRepository.update({ userId: exist.id }, { await this.userProfilesRepository.update({ userId: exist.id }, {
url, url,
fields, fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: _description,
birthday: bday?.[0] ?? null, birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null, location: person['vcard:Address'] ?? null,
listenbrainz: person.listenbrainz ?? null, listenbrainz: person.listenbrainz ?? null,

View file

@ -12,6 +12,7 @@ export interface IObject {
id?: string; id?: string;
name?: string | null; name?: string | null;
summary?: string; summary?: string;
_misskey_summary?: string;
published?: string; published?: string;
cc?: ApObject; cc?: ApObject;
to?: ApObject; to?: ApObject;

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { MiInstance } from '@/models/Instance.js'; import type { MiInstance } from '@/models/Instance.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';

View file

@ -77,7 +77,7 @@ export class NoteEntityService implements OnModuleInit {
@bindThis @bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) { private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false; let hide = false;
// visibility が specified かつ自分が指定されていなかったら非表示 // visibility が specified かつ自分が指定されていなかったら非表示
@ -87,7 +87,7 @@ export class NoteEntityService implements OnModuleInit {
} else if (meId === packedNote.userId) { } else if (meId === packedNote.userId) {
hide = false; hide = false;
} else { } else {
// 指定されているかどうか // 指定されているかどうか
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id); const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
if (specified) { if (specified) {
@ -187,27 +187,37 @@ export class NoteEntityService implements OnModuleInit {
} }
@bindThis @bindThis
public async populateMyReaction(noteId: MiNote['id'], meId: MiUser['id'], _hint_?: { public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
myReactions: Map<MiNote['id'], MiNoteReaction | null>; myReactions: Map<MiNote['id'], string | null>;
}) { }) {
if (_hint_?.myReactions) { if (_hint_?.myReactions) {
const reaction = _hint_.myReactions.get(noteId); const reaction = _hint_.myReactions.get(note.id);
if (reaction) { if (reaction) {
return this.reactionService.convertLegacyReaction(reaction.reaction); return this.reactionService.convertLegacyReaction(reaction);
} else if (reaction === null) { } else {
return undefined;
}
}
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) return undefined;
if (note.reactionAndUserPairCache && reactionsCount <= note.reactionAndUserPairCache.length) {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
if (pair) {
return this.reactionService.convertLegacyReaction(pair.split('/')[1]);
} else {
return undefined; return undefined;
} }
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
} }
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない // パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
if (this.idService.parse(noteId).date.getTime() + 2000 > Date.now()) { if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) {
return undefined; return undefined;
} }
const reaction = await this.noteReactionsRepository.findOneBy({ const reaction = await this.noteReactionsRepository.findOneBy({
userId: meId, userId: meId,
noteId: noteId, noteId: note.id,
}); });
if (reaction) { if (reaction) {
@ -293,8 +303,9 @@ export class NoteEntityService implements OnModuleInit {
options?: { options?: {
detail?: boolean; detail?: boolean;
skipHide?: boolean; skipHide?: boolean;
withReactionAndUserPairCache?: boolean;
_hint_?: { _hint_?: {
myReactions: Map<MiNote['id'], MiNoteReaction | null>; myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>; packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
}; };
}, },
@ -302,6 +313,7 @@ export class NoteEntityService implements OnModuleInit {
const opts = Object.assign({ const opts = Object.assign({
detail: true, detail: true,
skipHide: false, skipHide: false,
withReactionAndUserPairCache: false,
}, options); }, options);
const meId = me ? me.id : null; const meId = me ? me.id : null;
@ -343,6 +355,7 @@ export class NoteEntityService implements OnModuleInit {
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactions: this.reactionService.convertLegacyReactions(note.reactions), reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined, tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds, fileIds: note.fileIds,
@ -360,8 +373,8 @@ export class NoteEntityService implements OnModuleInit {
uri: note.uri ?? undefined, uri: note.uri ?? undefined,
url: note.url ?? undefined, url: note.url ?? undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
...(meId ? { ...(meId && Object.keys(note.reactions).length > 0 ? {
myReaction: this.populateMyReaction(note.id, meId, options?._hint_), myReaction: this.populateMyReaction(note, meId, options?._hint_),
} : {}), } : {}),
...(opts.detail ? { ...(opts.detail ? {
@ -369,11 +382,15 @@ export class NoteEntityService implements OnModuleInit {
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
detail: false, detail: false,
skipHide: opts.skipHide,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_, _hint_: options?._hint_,
}) : undefined, }) : undefined,
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
detail: true, detail: true,
skipHide: opts.skipHide,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_, _hint_: options?._hint_,
}) : undefined, }) : undefined,
} : {}), } : {}),
@ -398,19 +415,48 @@ export class NoteEntityService implements OnModuleInit {
if (notes.length === 0) return []; if (notes.length === 0) return [];
const meId = me ? me.id : null; const meId = me ? me.id : null;
const myReactionsMap = new Map<MiNote['id'], MiNoteReaction | null>(); const myReactionsMap = new Map<MiNote['id'], string | null>();
if (meId) { if (meId) {
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); const idsNeedFetchMyReaction = new Set<MiNote['id']>();
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない // パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
const oldId = this.idService.gen(Date.now() - 2000); const oldId = this.idService.gen(Date.now() - 2000);
const targets = [...notes.filter(n => n.id < oldId).map(n => n.id), ...renoteIds];
const myReactions = await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(targets),
});
for (const target of targets) { for (const note of notes) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null);
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
} else {
idsNeedFetchMyReaction.add(note.renote.id);
}
} else {
if (note.id < oldId) {
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.id, null);
} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
} else {
idsNeedFetchMyReaction.add(note.id);
}
} else {
myReactionsMap.set(note.id, null);
}
}
}
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(Array.from(idsNeedFetchMyReaction)),
}) : [];
for (const id of idsNeedFetchMyReaction) {
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
} }
} }

View file

@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, FollowRequestsRepository, NotesRepository, MiUser, UsersRepository } from '@/models/_.js'; import type { FollowRequestsRepository, NotesRepository, MiUser, UsersRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiNotification } from '@/models/Notification.js'; import type { MiNotification } from '@/models/Notification.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
@ -40,9 +40,6 @@ export class NotificationEntityService implements OnModuleInit {
@Inject(DI.followRequestsRepository) @Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository, private followRequestsRepository: FollowRequestsRepository,
@Inject(DI.accessTokensRepository)
private accessTokensRepository: AccessTokensRepository,
//private userEntityService: UserEntityService, //private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService, //private noteEntityService: NoteEntityService,
//private customEmojiService: CustomEmojiService, //private customEmojiService: CustomEmojiService,
@ -69,7 +66,6 @@ export class NotificationEntityService implements OnModuleInit {
}, },
): Promise<Packed<'Notification'>> { ): Promise<Packed<'Notification'>> {
const notification = src; const notification = src;
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? ( const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
hint?.packedNotes != null hint?.packedNotes != null
? hint.packedNotes.get(notification.noteId) ? hint.packedNotes.get(notification.noteId)
@ -100,8 +96,8 @@ export class NotificationEntityService implements OnModuleInit {
} : {}), } : {}),
...(notification.type === 'app' ? { ...(notification.type === 'app' ? {
body: notification.customBody, body: notification.customBody,
header: notification.customHeader ?? token?.name, header: notification.customHeader,
icon: notification.customIcon ?? token?.iconUrl, icon: notification.customIcon,
} : {}), } : {}),
}); });
} }

View file

@ -7,10 +7,12 @@ import { Injectable } from '@nestjs/common';
import type { } from '@/models/Blocking.js'; import type { } from '@/models/Blocking.js';
import type { MiSignin } from '@/models/Signin.js'; import type { MiSignin } from '@/models/Signin.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
@Injectable() @Injectable()
export class SigninEntityService { export class SigninEntityService {
constructor( constructor(
private idService: IdService,
) { ) {
} }
@ -18,7 +20,13 @@ export class SigninEntityService {
public async pack( public async pack(
src: MiSignin, src: MiSignin,
) { ) {
return src; return {
id: src.id,
createdAt: this.idService.parse(src.id).date.toISOString(),
ip: src.ip,
headers: src.headers,
success: src.success,
};
} }
} }

View file

@ -21,9 +21,10 @@ import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { AnnouncementService } from '../AnnouncementService.js';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js'; import type { PageEntityService } from './PageEntityService.js';
@ -62,6 +63,7 @@ export class UserEntityService implements OnModuleInit {
private roleService: RoleService; private roleService: RoleService;
private federatedInstanceService: FederatedInstanceService; private federatedInstanceService: FederatedInstanceService;
private idService: IdService; private idService: IdService;
private avatarDecorationService: AvatarDecorationService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@ -126,6 +128,7 @@ export class UserEntityService implements OnModuleInit {
this.roleService = this.moduleRef.get('RoleService'); this.roleService = this.moduleRef.get('RoleService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.idService = this.moduleRef.get('IdService'); this.idService = this.moduleRef.get('IdService');
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
} }
//#region Validators //#region Validators
@ -351,9 +354,11 @@ export class UserEntityService implements OnModuleInit {
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null; const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null;
const unreadAnnouncements = isMe && opts.detail ? await this.announcementService.getUnreadAnnouncements(user) : null; const unreadAnnouncements = isMe && opts.detail ?
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
const falsy = opts.detail ? false : undefined; createdAt: this.idService.parse(announcement.id).date.toISOString(),
...announcement,
})) : null;
const checkHost = user.host == null ? this.config.host : user.host; const checkHost = user.host == null ? this.config.host : user.host;
@ -366,10 +371,16 @@ export class UserEntityService implements OnModuleInit {
avatarBlurhash: user.avatarBlurhash, avatarBlurhash: user.avatarBlurhash,
description: mastoapi ? mastoapi.description : profile ? profile.description : '', description: mastoapi ? mastoapi.description : profile ? profile.description : '',
createdAt: this.idService.parse(user.id).date.toISOString(), createdAt: this.idService.parse(user.id).date.toISOString(),
avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({
id: ud.id,
angle: ud.angle || undefined,
flipH: ud.flipH || undefined,
url: decorations.find(d => d.id === ud.id)!.url,
}))) : [],
isBot: user.isBot, isBot: user.isBot,
isCat: user.isCat, isCat: user.isCat,
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
speakAsCat: user.speakAsCat ?? falsy, speakAsCat: user.speakAsCat ?? false,
approved: user.approved, approved: user.approved,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name, name: instance.name,

View file

@ -18,6 +18,7 @@ export const DI = {
announcementsRepository: Symbol('announcementsRepository'), announcementsRepository: Symbol('announcementsRepository'),
announcementReadsRepository: Symbol('announcementReadsRepository'), announcementReadsRepository: Symbol('announcementReadsRepository'),
appsRepository: Symbol('appsRepository'), appsRepository: Symbol('appsRepository'),
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteFavoritesRepository: Symbol('noteFavoritesRepository'),
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
noteReactionsRepository: Symbol('noteReactionsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'),

View file

@ -0,0 +1,10 @@
import type { MiNote } from '@/models/Note.js';
export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
if (!note.renoteId) return false;
if (note.text) return false; // it's quoted with text
if (note.fileIds.length !== 0) return false; // it's quoted with files
if (note.hasPoll) return false; // it's quoted with poll
return true;
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
import { id } from './util/id.js';
@Entity('avatar_decoration')
export class MiAvatarDecoration {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
nullable: true,
})
public updatedAt: Date | null;
@Column('varchar', {
length: 1024,
})
public url: string;
@Column('varchar', {
length: 256,
})
public name: string;
@Column('varchar', {
length: 2048,
})
public description: string;
// TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
@Column('varchar', {
array: true, length: 128, default: '{}',
})
public roleIdsThatCanBeUsedThisDecoration: string[];
}

View file

@ -504,6 +504,11 @@ export class MiMeta {
}) })
public preservedUsernames: string[]; public preservedUsernames: string[];
@Column('boolean', {
default: true,
})
public enableFanoutTimeline: boolean;
@Column('integer', { @Column('integer', {
default: 300, default: 300,
}) })

View file

@ -170,6 +170,11 @@ export class MiNote {
}) })
public mentionedRemoteUsers: string; public mentionedRemoteUsers: string;
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public reactionAndUserPairCache: string[];
@Column('varchar', { @Column('varchar', {
length: 128, array: true, default: '{}', length: 128, array: true, default: '{}',
}) })

View file

@ -5,7 +5,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit } from './_.js'; import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit } from './_.js';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
@ -39,6 +39,12 @@ const $appsRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $avatarDecorationsRepository: Provider = {
provide: DI.avatarDecorationsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration),
inject: [DI.db],
};
const $noteFavoritesRepository: Provider = { const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository, provide: DI.noteFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite), useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
@ -408,6 +414,7 @@ const $noteEditRepository: Provider = {
$announcementsRepository, $announcementsRepository,
$announcementReadsRepository, $announcementReadsRepository,
$appsRepository, $appsRepository,
$avatarDecorationsRepository,
$noteFavoritesRepository, $noteFavoritesRepository,
$noteThreadMutingsRepository, $noteThreadMutingsRepository,
$noteReactionsRepository, $noteReactionsRepository,
@ -475,6 +482,7 @@ const $noteEditRepository: Provider = {
$announcementsRepository, $announcementsRepository,
$announcementReadsRepository, $announcementReadsRepository,
$appsRepository, $appsRepository,
$avatarDecorationsRepository,
$noteFavoritesRepository, $noteFavoritesRepository,
$noteThreadMutingsRepository, $noteThreadMutingsRepository,
$noteReactionsRepository, $noteReactionsRepository,

View file

@ -160,6 +160,15 @@ export class MiUser {
length: 128, nullable: true, length: 128, nullable: true,
}) })
public backgroundBlurhash: string | null; public backgroundBlurhash: string | null;
@Column('jsonb', {
default: [],
})
public avatarDecorations: {
id: string;
angle: number;
flipH: boolean;
}[];
@Index() @Index()
@Column('varchar', { @Column('varchar', {

View file

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

View file

@ -174,6 +174,14 @@ export const packedNoteSchema = {
type: 'string', type: 'string',
optional: true, nullable: false, optional: true, nullable: false,
}, },
reactionAndUserPairCache: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
myReaction: { myReaction: {
type: 'object', type: 'object',

View file

@ -37,6 +37,34 @@ export const packedUserLiteSchema = {
type: 'string', type: 'string',
nullable: true, optional: false, nullable: true, optional: false,
}, },
avatarDecorations: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'object',
nullable: false, optional: false,
properties: {
id: {
type: 'string',
nullable: false, optional: false,
format: 'id',
},
url: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
angle: {
type: 'number',
nullable: false, optional: true,
},
flipH: {
type: 'boolean',
nullable: false, optional: true,
},
},
},
},
isAdmin: { isAdmin: {
type: 'boolean', type: 'boolean',
nullable: false, optional: true, nullable: false, optional: true,

View file

@ -18,6 +18,7 @@ import { MiAnnouncement } from '@/models/Announcement.js';
import { MiAnnouncementRead } from '@/models/AnnouncementRead.js'; import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
import { MiAntenna } from '@/models/Antenna.js'; import { MiAntenna } from '@/models/Antenna.js';
import { MiApp } from '@/models/App.js'; import { MiApp } from '@/models/App.js';
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
import { MiAuthSession } from '@/models/AuthSession.js'; import { MiAuthSession } from '@/models/AuthSession.js';
import { MiBlocking } from '@/models/Blocking.js'; import { MiBlocking } from '@/models/Blocking.js';
import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
@ -130,6 +131,7 @@ export const entities = [
MiMeta, MiMeta,
MiInstance, MiInstance,
MiApp, MiApp,
MiAvatarDecoration,
MiAuthSession, MiAuthSession,
MiAccessToken, MiAccessToken,
MiUser, MiUser,

View file

@ -26,6 +26,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js'; import { IActivity } from '@/core/activitypub/type.js';
import { isPureRenote } from '@/misc/is-pure-renote.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import type { FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
@ -88,7 +89,7 @@ export class ActivityPubServerService {
*/ */
@bindThis @bindThis
private async packActivity(note: MiNote): Promise<any> { private async packActivity(note: MiNote): Promise<any> {
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { if (isPureRenote(note)) {
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId }); const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note); return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
} }

View file

@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
public async launch(): Promise<void> { public async launch(): Promise<void> {
const fastify = Fastify({ const fastify = Fastify({
trustProxy: true, trustProxy: true,
logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''), logger: false,
}); });
this.#fastify = fastify; this.#fastify = fastify;

View file

@ -318,8 +318,9 @@ export class ApiCallService implements OnApplicationShutdown {
} }
if (ep.meta.requireRolePolicy != null && !user!.isRoot) { if (ep.meta.requireRolePolicy != null && !user!.isRoot) {
const myRoles = await this.roleService.getUserRoles(user!.id);
const policies = await this.roleService.getUserPolicies(user!.id); const policies = await this.roleService.getUserPolicies(user!.id);
if (!policies[ep.meta.requireRolePolicy]) { if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
throw new ApiError({ throw new ApiError({
message: 'You are not assigned to a required role.', message: 'You are not assigned to a required role.',
code: 'ROLE_PERMISSION_DENIED', code: 'ROLE_PERMISSION_DENIED',

View file

@ -18,6 +18,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement
import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
@ -166,6 +170,7 @@ import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js'; import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_update from './endpoints/following/update.js'; import * as ep___following_update from './endpoints/following/update.js';
import * as ep___following_update_all from './endpoints/following/update-all.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js';
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
@ -181,6 +186,7 @@ import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
import * as ep___hashtags_list from './endpoints/hashtags/list.js'; import * as ep___hashtags_list from './endpoints/hashtags/list.js';
import * as ep___hashtags_search from './endpoints/hashtags/search.js'; import * as ep___hashtags_search from './endpoints/hashtags/search.js';
import * as ep___hashtags_show from './endpoints/hashtags/show.js'; import * as ep___hashtags_show from './endpoints/hashtags/show.js';
@ -359,6 +365,7 @@ import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js'; import * as ep___retention from './endpoints/retention.js';
import * as ep___sponsors from './endpoints/sponsors.js'; import * as ep___sponsors from './endpoints/sponsors.js';
import { GetterService } from './GetterService.js'; import { GetterService } from './GetterService.js';
@ -377,6 +384,10 @@ const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements
const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default }; const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default };
const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default }; const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default };
const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default }; const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default };
const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-decorations/create', useClass: ep___admin_avatarDecorations_create.default };
const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default };
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
@ -525,6 +536,7 @@ const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass:
const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default }; const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default }; const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default }; const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default };
const $following_update_all: Provider = { provide: 'ep:following/update-all', useClass: ep___following_update_all.default };
const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default }; const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default }; const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default }; const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
@ -540,6 +552,7 @@ const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useCla
const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default }; const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default };
const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default }; const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default };
const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default }; const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default };
const $getAvatarDecorations: Provider = { provide: 'ep:get-avatar-decorations', useClass: ep___getAvatarDecorations.default };
const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default }; const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default };
const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default }; const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default };
const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default }; const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default };
@ -718,6 +731,7 @@ const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_s
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default }; const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.default }; const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.default };
@ -740,6 +754,10 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$admin_announcements_delete, $admin_announcements_delete,
$admin_announcements_list, $admin_announcements_list,
$admin_announcements_update, $admin_announcements_update,
$admin_avatarDecorations_create,
$admin_avatarDecorations_delete,
$admin_avatarDecorations_list,
$admin_avatarDecorations_update,
$admin_deleteAllFilesOfAUser, $admin_deleteAllFilesOfAUser,
$admin_drive_cleanRemoteFiles, $admin_drive_cleanRemoteFiles,
$admin_drive_cleanup, $admin_drive_cleanup,
@ -888,6 +906,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$following_create, $following_create,
$following_delete, $following_delete,
$following_update, $following_update,
$following_update_all,
$following_invalidate, $following_invalidate,
$following_requests_accept, $following_requests_accept,
$following_requests_cancel, $following_requests_cancel,
@ -903,6 +922,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$gallery_posts_unlike, $gallery_posts_unlike,
$gallery_posts_update, $gallery_posts_update,
$getOnlineUsersCount, $getOnlineUsersCount,
$getAvatarDecorations,
$hashtags_list, $hashtags_list,
$hashtags_search, $hashtags_search,
$hashtags_show, $hashtags_show,
@ -1081,6 +1101,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$users_achievements, $users_achievements,
$users_updateMemo, $users_updateMemo,
$fetchRss, $fetchRss,
$fetchExternalResources,
$retention, $retention,
$sponsors, $sponsors,
], ],
@ -1097,6 +1118,10 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$admin_announcements_delete, $admin_announcements_delete,
$admin_announcements_list, $admin_announcements_list,
$admin_announcements_update, $admin_announcements_update,
$admin_avatarDecorations_create,
$admin_avatarDecorations_delete,
$admin_avatarDecorations_list,
$admin_avatarDecorations_update,
$admin_deleteAllFilesOfAUser, $admin_deleteAllFilesOfAUser,
$admin_drive_cleanRemoteFiles, $admin_drive_cleanRemoteFiles,
$admin_drive_cleanup, $admin_drive_cleanup,
@ -1245,6 +1270,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$following_create, $following_create,
$following_delete, $following_delete,
$following_update, $following_update,
$following_update_all,
$following_invalidate, $following_invalidate,
$following_requests_accept, $following_requests_accept,
$following_requests_cancel, $following_requests_cancel,
@ -1260,6 +1286,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$gallery_posts_unlike, $gallery_posts_unlike,
$gallery_posts_update, $gallery_posts_update,
$getOnlineUsersCount, $getOnlineUsersCount,
$getAvatarDecorations,
$hashtags_list, $hashtags_list,
$hashtags_search, $hashtags_search,
$hashtags_show, $hashtags_show,
@ -1435,6 +1462,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
$users_achievements, $users_achievements,
$users_updateMemo, $users_updateMemo,
$fetchRss, $fetchRss,
$fetchExternalResources,
$retention, $retention,
$sponsors, $sponsors,
], ],

View file

@ -15,6 +15,7 @@ import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { UserService } from '@/core/UserService.js'; import { UserService } from '@/core/UserService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/Connection.js'; import MainStreamConnection from './stream/Connection.js';
import { ChannelsService } from './stream/ChannelsService.js'; import { ChannelsService } from './stream/ChannelsService.js';
@ -39,6 +40,7 @@ export class StreamingApiServerService {
private channelsService: ChannelsService, private channelsService: ChannelsService,
private notificationService: NotificationService, private notificationService: NotificationService,
private usersService: UserService, private usersService: UserService,
private channelFollowingService: ChannelFollowingService,
) { ) {
} }
@ -93,6 +95,7 @@ export class StreamingApiServerService {
this.noteReadService, this.noteReadService,
this.notificationService, this.notificationService,
this.cacheService, this.cacheService,
this.channelFollowingService,
user, app, user, app,
); );

View file

@ -18,6 +18,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement
import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
@ -166,6 +170,7 @@ import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js'; import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_update from './endpoints/following/update.js'; import * as ep___following_update from './endpoints/following/update.js';
import * as ep___following_update_all from './endpoints/following/update-all.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js';
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
@ -181,6 +186,7 @@ import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
import * as ep___hashtags_list from './endpoints/hashtags/list.js'; import * as ep___hashtags_list from './endpoints/hashtags/list.js';
import * as ep___hashtags_search from './endpoints/hashtags/search.js'; import * as ep___hashtags_search from './endpoints/hashtags/search.js';
import * as ep___hashtags_show from './endpoints/hashtags/show.js'; import * as ep___hashtags_show from './endpoints/hashtags/show.js';
@ -359,6 +365,7 @@ import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js'; import * as ep___retention from './endpoints/retention.js';
import * as ep___sponsors from './endpoints/sponsors.js'; import * as ep___sponsors from './endpoints/sponsors.js';
@ -375,6 +382,10 @@ const eps = [
['admin/announcements/delete', ep___admin_announcements_delete], ['admin/announcements/delete', ep___admin_announcements_delete],
['admin/announcements/list', ep___admin_announcements_list], ['admin/announcements/list', ep___admin_announcements_list],
['admin/announcements/update', ep___admin_announcements_update], ['admin/announcements/update', ep___admin_announcements_update],
['admin/avatar-decorations/create', ep___admin_avatarDecorations_create],
['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete],
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
['admin/drive/cleanup', ep___admin_drive_cleanup], ['admin/drive/cleanup', ep___admin_drive_cleanup],
@ -523,6 +534,7 @@ const eps = [
['following/create', ep___following_create], ['following/create', ep___following_create],
['following/delete', ep___following_delete], ['following/delete', ep___following_delete],
['following/update', ep___following_update], ['following/update', ep___following_update],
['following/update-all', ep___following_update_all],
['following/invalidate', ep___following_invalidate], ['following/invalidate', ep___following_invalidate],
['following/requests/accept', ep___following_requests_accept], ['following/requests/accept', ep___following_requests_accept],
['following/requests/cancel', ep___following_requests_cancel], ['following/requests/cancel', ep___following_requests_cancel],
@ -538,6 +550,7 @@ const eps = [
['gallery/posts/unlike', ep___gallery_posts_unlike], ['gallery/posts/unlike', ep___gallery_posts_unlike],
['gallery/posts/update', ep___gallery_posts_update], ['gallery/posts/update', ep___gallery_posts_update],
['get-online-users-count', ep___getOnlineUsersCount], ['get-online-users-count', ep___getOnlineUsersCount],
['get-avatar-decorations', ep___getAvatarDecorations],
['hashtags/list', ep___hashtags_list], ['hashtags/list', ep___hashtags_list],
['hashtags/search', ep___hashtags_search], ['hashtags/search', ep___hashtags_search],
['hashtags/show', ep___hashtags_show], ['hashtags/show', ep___hashtags_show],
@ -716,6 +729,7 @@ const eps = [
['users/achievements', ep___users_achievements], ['users/achievements', ep___users_achievements],
['users/update-memo', ep___users_updateMemo], ['users/update-memo', ep___users_updateMemo],
['fetch-rss', ep___fetchRss], ['fetch-rss', ep___fetchRss],
['fetch-external-resources', ep___fetchExternalResources],
['retention', ep___retention], ['retention', ep___retention],
['sponsors', ep___sponsors], ['sponsors', ep___sponsors],
]; ];

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
} as const;
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
description: { type: 'string' },
url: { type: 'string', minLength: 1 },
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
type: 'string',
} },
},
required: ['name', 'description', 'url'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
) {
super(meta, paramDef, async (ps, me) => {
await this.avatarDecorationService.create({
name: ps.name,
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
}, me);
});
}
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
) {
super(meta, paramDef, async (ps, me) => {
await this.avatarDecorationService.delete(ps.id, me);
});
}
}

View file

@ -0,0 +1,101 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
import type { MiAnnouncement } from '@/models/Announcement.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
name: {
type: 'string',
optional: false, nullable: false,
},
description: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
},
roleIdsThatCanBeUsedThisDecoration: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const avatarDecorations = await this.avatarDecorationService.getAll(true);
return avatarDecorations.map(avatarDecoration => ({
id: avatarDecoration.id,
createdAt: this.idService.parse(avatarDecoration.id).date.toISOString(),
updatedAt: avatarDecoration.updatedAt?.toISOString() ?? null,
name: avatarDecoration.name,
description: avatarDecoration.description,
url: avatarDecoration.url,
roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration,
}));
});
}
}

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1 },
description: { type: 'string' },
url: { type: 'string', minLength: 1 },
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
type: 'string',
} },
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
) {
super(meta, paramDef, async (ps, me) => {
await this.avatarDecorationService.update(ps.id, {
name: ps.name,
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
}, me);
});
}
}

View file

@ -110,11 +110,11 @@ export const meta = {
optional: false, nullable: false, optional: false, nullable: false,
}, },
silencedHosts: { silencedHosts: {
type: "array", type: 'array',
optional: true, optional: true,
nullable: false, nullable: false,
items: { items: {
type: "string", type: 'string',
optional: false, optional: false,
nullable: false, nullable: false,
}, },
@ -303,6 +303,10 @@ export const meta = {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
}, },
enableFanoutTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
perLocalUserUserTimelineCacheMax: { perLocalUserUserTimelineCacheMax: {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
@ -434,6 +438,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableIdenticonGeneration: instance.enableIdenticonGeneration, enableIdenticonGeneration: instance.enableIdenticonGeneration,
policies: { ...DEFAULT_POLICIES, ...instance.policies }, policies: { ...DEFAULT_POLICIES, ...instance.policies },
manifestJsonOverride: instance.manifestJsonOverride, manifestJsonOverride: instance.manifestJsonOverride,
enableFanoutTimeline: instance.enableFanoutTimeline,
perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax, perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,

View file

@ -123,6 +123,7 @@ export const paramDef = {
serverRules: { type: 'array', items: { type: 'string' } }, serverRules: { type: 'array', items: { type: 'string' } },
preservedUsernames: { type: 'array', items: { type: 'string' } }, preservedUsernames: { type: 'array', items: { type: 'string' } },
manifestJsonOverride: { type: 'string' }, manifestJsonOverride: { type: 'string' },
enableFanoutTimeline: { type: 'boolean' },
perLocalUserUserTimelineCacheMax: { type: 'integer' }, perLocalUserUserTimelineCacheMax: { type: 'integer' },
perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' },
@ -495,6 +496,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.manifestJsonOverride = ps.manifestJsonOverride; set.manifestJsonOverride = ps.manifestJsonOverride;
} }
if (ps.enableFanoutTimeline !== undefined) {
set.enableFanoutTimeline = ps.enableFanoutTimeline;
}
if (ps.perLocalUserUserTimelineCacheMax !== undefined) { if (ps.perLocalUserUserTimelineCacheMax !== undefined) {
set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax; set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax;
} }

View file

@ -5,9 +5,9 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/_.js'; import type { ChannelsRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -41,11 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.channelsRepository) @Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository, private channelsRepository: ChannelsRepository,
private channelFollowingService: ChannelFollowingService,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({ const channel = await this.channelsRepository.findOneBy({
@ -56,11 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchChannel); throw new ApiError(meta.errors.noSuchChannel);
} }
await this.channelFollowingsRepository.insert({ await this.channelFollowingService.follow(me, channel);
id: this.idService.gen(),
followerId: me.id,
followeeId: channel.id,
});
}); });
} }
} }

View file

@ -5,8 +5,9 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/_.js'; import type { ChannelsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -40,9 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.channelsRepository) @Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository, private channelsRepository: ChannelsRepository,
private channelFollowingService: ChannelFollowingService,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({ const channel = await this.channelsRepository.findOneBy({
@ -53,10 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchChannel); throw new ApiError(meta.errors.noSuchChannel);
} }
await this.channelFollowingsRepository.delete({ await this.channelFollowingService.unfollow(me, channel);
followerId: me.id,
followeeId: channel.id,
});
}); });
} }
} }

View file

@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createHash } from 'crypto';
import ms from 'ms';
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { ApiError } from '../error.js';
export const meta = {
tags: ['meta'],
requireCredential: true,
limit: {
duration: ms('1hour'),
max: 50,
},
errors: {
invalidSchema: {
message: 'External resource returned invalid schema.',
code: 'EXT_RESOURCE_RETURNED_INVALID_SCHEMA',
id: 'bb774091-7a15-4a70-9dc5-6ac8cf125856',
},
hashUnmached: {
message: 'Hash did not match.',
code: 'EXT_RESOURCE_HASH_DIDNT_MATCH',
id: '693ba8ba-b486-40df-a174-72f8279b56a4',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
url: { type: 'string' },
hash: { type: 'string' },
},
required: ['url', 'hash'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private httpRequestService: HttpRequestService,
) {
super(meta, paramDef, async (ps) => {
const res = await this.httpRequestService.getJson<{
type: string;
data: string;
}>(ps.url);
if (!res.data || !res.type) {
throw new ApiError(meta.errors.invalidSchema);
}
const resHash = createHash('sha512').update(res.data.replace(/\r\n/g, '\n')).digest('hex');
if (resHash !== ps.hash) {
throw new ApiError(meta.errors.hashUnmached);
}
return {
type: res.type,
data: res.data,
};
});
}
}

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FollowingsRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['following', 'users'],
limit: {
duration: ms('1hour'),
max: 10,
},
requireCredential: true,
kind: 'write:following',
} as const;
export const paramDef = {
type: 'object',
properties: {
notify: { type: 'string', enum: ['normal', 'none'] },
withReplies: { type: 'boolean' },
},
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
) {
super(meta, paramDef, async (ps, me) => {
await this.followingsRepository.update({
followerId: me.id,
}, {
notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined,
withReplies: ps.withReplies != null ? ps.withReplies : undefined,
});
return;
});
}
}

View file

@ -0,0 +1,82 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['users'],
requireCredential: false,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
name: {
type: 'string',
optional: false, nullable: false,
},
description: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
},
roleIdsThatCanBeUsedThisDecoration: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const decorations = await this.avatarDecorationService.getAll(true);
const allRoles = await this.roleService.getRoles();
return decorations.map(decoration => ({
id: decoration.id,
name: decoration.name,
description: decoration.description,
url: decoration.url,
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)),
}));
});
}
}

View file

@ -18,8 +18,12 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
tokenId: { type: 'string', format: 'misskey:id' }, tokenId: { type: 'string', format: 'misskey:id' },
token: { type: 'string' },
}, },
required: ['tokenId'], anyOf: [
{ required: ['tokenId'] },
{ required: ['token'] },
],
} as const; } as const;
@Injectable() @Injectable()
@ -29,13 +33,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accessTokensRepository: AccessTokensRepository, private accessTokensRepository: AccessTokensRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } }); if (ps.tokenId) {
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
if (tokenExist) { if (tokenExist) {
await this.accessTokensRepository.delete({ await this.accessTokensRepository.delete({
id: ps.tokenId, id: ps.tokenId,
userId: me.id, userId: me.id,
}); });
}
} else if (ps.token) {
const tokenExist = await this.accessTokensRepository.exist({ where: { token: ps.token } });
if (tokenExist) {
await this.accessTokensRepository.delete({
token: ps.token,
userId: me.id,
});
}
} }
}); });
} }

View file

@ -32,6 +32,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { safeForSql } from '@/misc/safe-for-sql.js'; import { safeForSql } from '@/misc/safe-for-sql.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -144,6 +145,15 @@ export const paramDef = {
listenbrainz: { ...listenbrainzSchema, nullable: true }, listenbrainz: { ...listenbrainzSchema, nullable: true },
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
avatarId: { type: 'string', format: 'misskey:id', nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true },
avatarDecorations: { type: 'array', maxItems: 1, items: {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
angle: { type: 'number', nullable: true, maximum: 0.5, minimum: -0.5 },
flipH: { type: 'boolean', nullable: true },
},
required: ['id'],
} },
bannerId: { type: 'string', format: 'misskey:id', nullable: true }, bannerId: { type: 'string', format: 'misskey:id', nullable: true },
backgroundId: { type: 'string', format: 'misskey:id', nullable: true }, backgroundId: { type: 'string', format: 'misskey:id', nullable: true },
fields: { fields: {
@ -222,6 +232,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService, private roleService: RoleService,
private cacheService: CacheService, private cacheService: CacheService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private avatarDecorationService: AvatarDecorationService,
) { ) {
super(meta, paramDef, async (ps, _user, token) => { super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@ -327,6 +338,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.backgroundUrl = null; updates.backgroundUrl = null;
updates.backgroundBlurhash = null; updates.backgroundBlurhash = null;
} }
if (ps.avatarDecorations) {
const decorations = await this.avatarDecorationService.getAll(true);
const myRoles = await this.roleService.getUserRoles(user.id);
const allRoles = await this.roleService.getRoles();
const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.map(d => d.id);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
id: d.id,
angle: d.angle ?? 0,
flipH: d.flipH ?? false,
}));
}
if (ps.pinnedPageId) { if (ps.pinnedPageId) {
const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId }); const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
@ -453,9 +479,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const myLink = `${this.config.url}/@${user.username}`; const myLink = `${this.config.url}/@${user.username}`;
const includesMyLink = Array.from(doc.getElementsByTagName('a')).some(a => a.href === myLink); const aEls = Array.from(doc.getElementsByTagName('a'));
const linkEls = Array.from(doc.getElementsByTagName('link'));
if (includesMyLink) { const includesMyLink = aEls.some(a => a.href === myLink);
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update() await this.userProfilesRepository.createQueryBuilder('profile').update()
.where('userId = :userId', { userId: user.id }) .where('userId = :userId', { userId: user.id })
.set({ .set({

View file

@ -17,6 +17,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { isPureRenote } from '@/misc/is-pure-renote.js';
export const meta = { export const meta = {
tags: ['notes'], tags: ['notes'],
@ -221,7 +222,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (renote == null) { if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget); throw new ApiError(meta.errors.noSuchRenoteTarget);
} else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { } else if (isPureRenote(renote)) {
throw new ApiError(meta.errors.cannotReRenote); throw new ApiError(meta.errors.cannotReRenote);
} }
@ -254,7 +255,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (reply == null) { if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget); throw new ApiError(meta.errors.noSuchReplyTarget);
} else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { } else if (isPureRenote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote); throw new ApiError(meta.errors.cannotReplyToPureRenote);
} }

View file

@ -5,7 +5,7 @@
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js'; import type { NotesRepository, FollowingsRepository, MiNote, ChannelFollowingsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@ -17,6 +17,8 @@ import { CacheService } from '@/core/CacheService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -68,6 +70,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private roleService: RoleService, private roleService: RoleService,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
@ -76,6 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private funoutTimelineService: FunoutTimelineService, private funoutTimelineService: FunoutTimelineService,
private queryService: QueryService, private queryService: QueryService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -86,171 +92,224 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.stlDisabled); throw new ApiError(meta.errors.stlDisabled);
} }
const [ const serverSettings = await this.metaService.fetch();
followings,
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(me.id),
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.renoteMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]);
let noteIds: string[]; if (serverSettings.enableFanoutTimeline) {
let shouldFallbackToDb = false; const [
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
] = await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.renoteMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]);
if (ps.withFiles) { let noteIds: string[];
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ let shouldFallbackToDb = false;
`homeTimelineWithFiles:${me.id}`,
'localTimelineWithFiles',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
} else if (ps.withReplies) {
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
'localTimelineWithReplies',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
} else {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
shouldFallbackToDb = htlNoteIds.length === 0;
}
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);
if (!shouldFallbackToDb) {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
let timeline = await query.getMany();
timeline = timeline.filter(note => {
if (note.userId === me.id) {
return true;
}
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
}
if (note.user?.isSilenced && note.userId !== me.id && !followings[note.userId]) return false;
return true;
});
// TODO: フィルタした結果件数が足りなかった場合の対応
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
} else { // fallback to db
const followees = await this.userFollowingService.getFollowees(me.id);
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(new Brackets(qb => {
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
} else {
qb.where('note.userId = :meId', { meId: me.id });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimelineWithFiles:${me.id}`,
'localTimelineWithFiles',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
} else if (ps.withReplies) {
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
'localTimelineWithReplies',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
} else {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
shouldFallbackToDb = htlNoteIds.length === 0;
} }
//#endregion
let timeline = await query.limit(ps.limit).getMany(); noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);
timeline = timeline.filter(note => { shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
if (note.user?.isSilenced && note.userId !== me.id && !followings[note.userId]) return false;
return true;
});
process.nextTick(() => { let redisTimeline: MiNote[] = [];
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me); if (!shouldFallbackToDb) {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
redisTimeline = await query.getMany();
redisTimeline = redisTimeline.filter(note => {
if (note.userId === me.id) {
return true;
}
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
}
if (!ps.withBots && note.user?.isBot) return false;
return true;
});
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
}
if (redisTimeline.length > 0) {
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(redisTimeline, me);
} else { // fallback to db
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
includeMyRenotes: ps.includeMyRenotes,
includeRenotedMyNotes: ps.includeRenotedMyNotes,
includeLocalRenotes: ps.includeLocalRenotes,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
withBots: ps.withBots,
}, me);
}
} else {
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
includeMyRenotes: ps.includeMyRenotes,
includeRenotedMyNotes: ps.includeRenotedMyNotes,
includeLocalRenotes: ps.includeLocalRenotes,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
withBots: ps.withBots,
}, me);
} }
}); });
} }
private async getFromDb(ps: {
untilId: string | null,
sinceId: string | null,
limit: number,
includeMyRenotes: boolean,
includeRenotedMyNotes: boolean,
includeLocalRenotes: boolean,
withFiles: boolean,
withReplies: boolean,
withBots: boolean,
}, me: MiLocalUser) {
const followees = await this.userFollowingService.getFollowees(me.id);
const followingChannels = await this.channelFollowingsRepository.find({
where: {
followerId: me.id,
},
});
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => {
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
} else {
qb.where('note.userId = :meId', { meId: me.id });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (followingChannels.length > 0) {
const followingChannelIds = followingChannels.map(x => x.followeeId);
query.andWhere(new Brackets(qb => {
qb.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
qb.orWhere('note.channelId IS NULL');
}));
} else {
query.andWhere('note.channelId IS NULL');
}
if (!ps.withReplies) {
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
//#endregion
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
}
} }

View file

@ -16,6 +16,8 @@ import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -70,6 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService, private cacheService: CacheService,
private funoutTimelineService: FunoutTimelineService, private funoutTimelineService: FunoutTimelineService,
private queryService: QueryService, private queryService: QueryService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -80,112 +83,148 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.ltlDisabled); throw new ApiError(meta.errors.ltlDisabled);
} }
const [ const serverSettings = await this.metaService.fetch();
followings,
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
] = me ? await Promise.all([
this.cacheService.userFollowingsCache.fetch(me.id),
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.renoteMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]) : [undefined, new Set<string>(), new Set<string>(), new Set<string>()];
let noteIds: string[]; if (serverSettings.enableFanoutTimeline) {
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
] = me ? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.renoteMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
if (ps.withFiles) { let noteIds: string[];
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
} else {
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
'localTimeline',
'localTimelineWithReplies',
], untilId, sinceId);
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
noteIds.sort((a, b) => a > b ? -1 : 1);
}
noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length > 0) {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
let timeline = await query.getMany();
timeline = timeline.filter(note => {
if (me && (note.userId === me.id)) {
return true;
}
if (!ps.withReplies && note.replyId && (me == null || note.replyUserId !== me.id)) return false;
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
}
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
return true;
});
// TODO: フィルタした結果件数が足りなかった場合の対応
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
process.nextTick(() => {
if (me) {
this.activeUsersChart.read(me);
}
});
return await this.noteEntityService.packMany(timeline, me);
} else { // fallback to db
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
} else {
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
'localTimeline',
'localTimelineWithReplies',
], untilId, sinceId);
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
noteIds.sort((a, b) => a > b ? -1 : 1);
} }
let timeline = await query.limit(ps.limit).getMany(); noteIds = noteIds.slice(0, ps.limit);
timeline = timeline.filter(note => { let redisTimeline: MiNote[] = [];
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
return true;
});
process.nextTick(() => { if (noteIds.length > 0) {
if (me) { const query = this.notesRepository.createQueryBuilder('note')
this.activeUsersChart.read(me); .where('note.id IN (:...noteIds)', { noteIds: noteIds })
} .innerJoinAndSelect('note.user', 'user')
}); .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
return await this.noteEntityService.packMany(timeline, me); redisTimeline = await query.getMany();
redisTimeline = redisTimeline.filter(note => {
if (me && (note.userId === me.id)) {
return true;
}
if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false;
if (!ps.withBots && note.user?.isBot) return false;
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
}
return true;
});
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
}
if (redisTimeline.length > 0) {
process.nextTick(() => {
if (me) {
this.activeUsersChart.read(me);
}
});
return await this.noteEntityService.packMany(redisTimeline, me);
} else { // fallback to db
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
withBots: ps.withBots,
}, me);
}
} else {
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
withBots: ps.withBots,
}, me);
} }
}); });
} }
private async getFromDb(ps: {
sinceId: string | null,
untilId: string | null,
limit: number,
withFiles: boolean,
withReplies: boolean,
withBots: boolean,
}, me: MiLocalUser | null) {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId)
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (!ps.withReplies) {
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
}
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
if (me) {
this.activeUsersChart.read(me);
}
});
return await this.noteEntityService.packMany(timeline, me);
}
} }

View file

@ -5,7 +5,7 @@
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js'; import type { MiNote, NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
@ -16,6 +16,8 @@ import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
export const meta = { export const meta = {
tags: ['notes'], tags: ['notes'],
@ -57,6 +59,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private idService: IdService, private idService: IdService,
@ -64,154 +69,214 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private funoutTimelineService: FunoutTimelineService, private funoutTimelineService: FunoutTimelineService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private queryService: QueryService, private queryService: QueryService,
private metaService: MetaService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const [ const serverSettings = await this.metaService.fetch();
followings,
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(me.id),
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.renoteMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]);
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); if (serverSettings.enableFanoutTimeline) {
noteIds = noteIds.slice(0, ps.limit); const [
followings,
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(me.id),
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.renoteMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]);
if (noteIds.length > 0) { let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
const query = this.notesRepository.createQueryBuilder('note') noteIds = noteIds.slice(0, ps.limit);
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
if (!ps.withBots) query.andWhere('user.isBot = FALSE'); let redisTimeline: MiNote[] = [];
let timeline = await query.getMany(); if (noteIds.length > 0) {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
timeline = timeline.filter(note => { redisTimeline = await query.getMany();
if (note.userId === me.id) {
return true; redisTimeline = redisTimeline.filter(note => {
} if (note.userId === me.id) {
if (isUserRelated(note, userIdsWhoBlockingMe)) return false; return true;
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
} }
} if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (note.reply && note.reply.visibility === 'followers') { if (isUserRelated(note, userIdsWhoMeMuting)) return false;
if (!Object.hasOwn(followings, note.reply.userId)) return false; if (note.renoteId) {
} if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (note.user?.isSilenced && note.userId !== me.id && !followings[note.userId]) return false; if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
}
if (note.reply && note.reply.visibility === 'followers') {
if (!Object.hasOwn(followings, note.reply.userId)) return false;
}
if (!ps.withBots && note.user?.isBot) return false;
return true; return true;
}); });
// TODO: フィルタした結果件数が足りなかった場合の対応 redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
} else { // fallback to db
const followees = await this.userFollowingService.getFollowees(me.id);
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.channelId IS NULL')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
} else {
query.andWhere('note.userId = :meId', { meId: me.id });
} }
query.andWhere(new Brackets(qb => { if (redisTimeline.length > 0) {
qb process.nextTick(() => {
.where('note.replyId IS NULL') // 返信ではない this.activeUsersChart.read(me);
.orWhere(new Brackets(qb => { });
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
this.queryService.generateVisibilityQuery(query, me); return await this.noteEntityService.packMany(redisTimeline, me);
this.queryService.generateMutedUserQuery(query, me); } else { // fallback to db
this.queryService.generateBlockedUserQuery(query, me); return await this.getFromDb({
this.queryService.generateMutedUserRenotesQueryForNotes(query, me); untilId,
sinceId,
if (ps.includeMyRenotes === false) { limit: ps.limit,
query.andWhere(new Brackets(qb => { includeMyRenotes: ps.includeMyRenotes,
qb.orWhere('note.userId != :meId', { meId: me.id }); includeRenotedMyNotes: ps.includeRenotedMyNotes,
qb.orWhere('note.renoteId IS NULL'); includeLocalRenotes: ps.includeLocalRenotes,
qb.orWhere('note.text IS NOT NULL'); withFiles: ps.withFiles,
qb.orWhere('note.fileIds != \'{}\''); withRenotes: ps.withRenotes,
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); withBots: ps.withBots,
})); }, me);
} }
} else {
if (ps.includeRenotedMyNotes === false) { return await this.getFromDb({
query.andWhere(new Brackets(qb => { untilId,
qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); sinceId,
qb.orWhere('note.renoteId IS NULL'); limit: ps.limit,
qb.orWhere('note.text IS NOT NULL'); includeMyRenotes: ps.includeMyRenotes,
qb.orWhere('note.fileIds != \'{}\''); includeRenotedMyNotes: ps.includeRenotedMyNotes,
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); includeLocalRenotes: ps.includeLocalRenotes,
})); withFiles: ps.withFiles,
} withRenotes: ps.withRenotes,
withBots: ps.withBots,
if (ps.includeLocalRenotes === false) { }, me);
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
let timeline = await query.limit(ps.limit).getMany();
timeline = timeline.filter(note => {
if (note.user?.isSilenced && note.userId !== me.id && !followings[note.userId]) return false;
return true;
});
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
} }
}); });
} }
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; withBots: boolean; }, me: MiLocalUser) {
const followees = await this.userFollowingService.getFollowees(me.id);
const followingChannels = await this.channelFollowingsRepository.find({
where: {
followerId: me.id,
},
});
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (followees.length > 0 && followingChannels.length > 0) {
// ユーザー・チャンネルともにフォローあり
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
const followingChannelIds = followingChannels.map(x => x.followeeId);
query.andWhere(new Brackets(qb => {
qb
.where(new Brackets(qb2 => {
qb2
.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds })
.andWhere('note.channelId IS NULL');
}))
.orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
}));
} else if (followees.length > 0) {
// ユーザーフォローのみ(チャンネルフォローなし)
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
query
.andWhere('note.channelId IS NULL')
.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
} else if (followingChannels.length > 0) {
// チャンネルフォローのみ(ユーザーフォローなし)
const followingChannelIds = followingChannels.map(x => x.followeeId);
query.andWhere(new Brackets(qb => {
qb
.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds })
.orWhere('note.userId = :meId', { meId: me.id });
}));
} else {
// フォローなし
query
.andWhere('note.channelId IS NULL')
.andWhere('note.userId = :meId', { meId: me.id });
}
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withRenotes === false) {
query.andWhere('note.renoteId IS NULL');
}
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
//#endregion
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
}
} }

View file

@ -3,12 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import type { MiNote, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -16,7 +13,9 @@ import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { Brackets } from 'typeorm';
export const meta = { export const meta = {
tags: ['notes', 'lists'], tags: ['notes', 'lists'],
@ -67,20 +66,22 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private cacheService: CacheService, private cacheService: CacheService,
private idService: IdService, private idService: IdService,
private funoutTimelineService: FunoutTimelineService, private funoutTimelineService: FunoutTimelineService,
private queryService: QueryService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -108,44 +109,129 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId); let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
noteIds = noteIds.slice(0, ps.limit); noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length === 0) { let redisTimeline: MiNote[] = [];
return [];
if (noteIds.length > 0) {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
redisTimeline = await query.getMany();
redisTimeline = redisTimeline.filter(note => {
if (note.userId === me.id) {
return true;
}
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
}
return true;
});
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
} }
const query = this.notesRepository.createQueryBuilder('note') if (redisTimeline.length > 0) {
.where('note.id IN (:...noteIds)', { noteIds: noteIds }) this.activeUsersChart.read(me);
.innerJoinAndSelect('note.user', 'user') return await this.noteEntityService.packMany(redisTimeline, me);
.leftJoinAndSelect('note.reply', 'reply') } else { // fallback to db
.leftJoinAndSelect('note.renote', 'renote') //#region Construct query
.leftJoinAndSelect('reply.user', 'replyUser') const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.leftJoinAndSelect('renote.user', 'renoteUser') .innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
.leftJoinAndSelect('note.channel', 'channel'); .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
.andWhere('note.channelId IS NULL') // チャンネルノートではない
.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}))
.orWhere(new Brackets(qb => {
qb // 返信だけど自分宛ての返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = :meId', { meId: me.id });
}))
.orWhere(new Brackets(qb => {
qb // 返信だけどwithRepliesがtrueの場合
.where('note.replyId IS NOT NULL')
.andWhere('userListMemberships.withReplies = true');
}));
}));
let timeline = await query.getMany(); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
timeline = timeline.filter(note => { if (ps.includeMyRenotes === false) {
if (note.userId === me.id) { query.andWhere(new Brackets(qb => {
return true; qb.orWhere('note.userId != :meId', { meId: me.id });
} qb.orWhere('note.renoteId IS NULL');
if (isUserRelated(note, userIdsWhoBlockingMe)) return false; qb.orWhere('note.text IS NOT NULL');
if (isUserRelated(note, userIdsWhoMeMuting)) return false; qb.orWhere('note.fileIds != \'{}\'');
if (note.renoteId) { qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { }));
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
} }
return true; if (ps.includeRenotedMyNotes === false) {
}); query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
// TODO: フィルタした結果件数が足りなかった場合の対応 if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
timeline.sort((a, b) => a.id > b.id ? -1 : 1); if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
this.activeUsersChart.read(me); if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
//#endregion
return await this.noteEntityService.packMany(timeline, me); const timeline = await query.limit(ps.limit).getMany();
this.activeUsersChart.read(me);
return await this.noteEntityService.packMany(timeline, me);
}
}); });
} }
} }

View file

@ -42,8 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.notificationService.createNotification(user.id, 'app', { this.notificationService.createNotification(user.id, 'app', {
appAccessTokenId: token ? token.id : null, appAccessTokenId: token ? token.id : null,
customBody: ps.body, customBody: ps.body,
customHeader: ps.header, customHeader: ps.header ?? token?.name,
customIcon: ps.icon, customIcon: ps.icon ?? token?.iconUrl,
}); });
}); });
} }

View file

@ -26,7 +26,12 @@ export function convertSchemaToOpenApiSchema(schema: Schema) {
if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema); if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema);
if (schema.ref) { if (schema.ref) {
res.$ref = `#/components/schemas/${schema.ref}`; const $ref = `#/components/schemas/${schema.ref}`;
if (schema.nullable || schema.optional) {
res.allOf = [{ $ref }];
} else {
res.$ref = $ref;
}
} }
return res; return res;

View file

@ -13,6 +13,7 @@ import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js'; import { MiFollowing, MiUserProfile } from '@/models/_.js';
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import type { ChannelsService } from './ChannelsService.js'; import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
import type Channel from './channel.js'; import type Channel from './channel.js';
@ -42,6 +43,7 @@ export default class Connection {
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private notificationService: NotificationService, private notificationService: NotificationService,
private cacheService: CacheService, private cacheService: CacheService,
private channelFollowingService: ChannelFollowingService,
user: MiUser | null | undefined, user: MiUser | null | undefined,
token: MiAccessToken | null | undefined, token: MiAccessToken | null | undefined,
@ -56,7 +58,7 @@ export default class Connection {
const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([ const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([
this.cacheService.userProfileCache.fetch(this.user.id), this.cacheService.userProfileCache.fetch(this.user.id),
this.cacheService.userFollowingsCache.fetch(this.user.id), this.cacheService.userFollowingsCache.fetch(this.user.id),
this.cacheService.userFollowingChannelsCache.fetch(this.user.id), this.channelFollowingService.userFollowingChannelsCache.fetch(this.user.id),
this.cacheService.userMutingsCache.fetch(this.user.id), this.cacheService.userMutingsCache.fetch(this.user.id),
this.cacheService.userBlockedCache.fetch(this.user.id), this.cacheService.userBlockedCache.fetch(this.user.id),
this.cacheService.renoteMutingsCache.fetch(this.user.id), this.cacheService.renoteMutingsCache.fetch(this.user.id),

View file

@ -67,6 +67,8 @@ export default abstract class Channel {
} }
public abstract init(params: any): void; public abstract init(params: any): void;
public dispose?(): void; public dispose?(): void;
public onMessage?(type: string, body: any): void; public onMessage?(type: string, body: any): void;
} }

View file

@ -46,8 +46,10 @@ class ChannelChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

View file

@ -77,8 +77,10 @@ class GlobalTimelineChannel extends Channel {
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
if (this.user && note.renoteId && !note.text) { if (this.user && note.renoteId && !note.text) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renoteId, this.user.id); if (note.renote && Object.keys(note.renote.reactions).length > 0) {
note.renote!.myReaction = myRenoteReaction; const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
} }
this.connection.cacheNote(note); this.connection.cacheNote(note);

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